Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
multimedia:proaudio
new-session-manager
new-session-manager-1.6.0+git.20220415.0f6719c....
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File new-session-manager-1.6.0+git.20220415.0f6719c.obscpio of Package new-session-manager
07070100000000000081A40000000000000000000000016258A65C00000073000000000000000000000000000000000000003A00000000new-session-manager-1.6.0+git.20220415.0f6719c/.gitignore*~ *.bak *.swp *.[ao] TAGS .nfs* build builddir subprojects # Ignore (compiled) python files, *__pycache__/* *.pyc 07070100000001000081A40000000000000000000000016258A65C000001F1000000000000000000000000000000000000003700000000new-session-manager-1.6.0+git.20220415.0f6719c/AUTHORS# A complete list of authors. For individual contributions please read CHANGELOG Liles, Jonathan Moore / original-male (original author) for authors before the fork in May 2020 please see https://github.com/original-male/non Coelho, Filipe / falktx (new-session-manager fork) Hilbricht, Nils (new-session-manager fork) # Individual Contributions in alphabetical surname order Meyer, Hermann / brummer Picot, Mathieu / houston Berkelder, Rik Runge, David / dvzrv / TheGreatWhiteShark 07070100000002000081A40000000000000000000000016258A65C00001771000000000000000000000000000000000000003900000000new-session-manager-1.6.0+git.20220415.0f6719c/CHANGELOG#Changelog Format: Double ## for a version number followed by a space, ISO-Date, Semantic Versioning: ## YYYY-MM-DD major.minor.patch Two empty lines before the next entry. External contributors notice at the end of the line: (LastName, FirstName / nick) ## 2022-04-15 1.6.0 nsmd: Now follows the XDG Base Directory Specifications. Default session directory moved from ~/NSM Sessions/ to $XDG_DATA_HOME/nsm/ (see issue #gh-15) The old path ~/NSM Sessions/ is still supported and has priority, for now. This may be switched off in the future. Lockfiles fixed (see issue #gh-31) Lockfiles are now in $XDG_RUNTIME_DIR/nsm/ Lockfiles now each contain the session path, the osc NSM_URL and the nsmd PID One daemon file for each currently running nsmd is created in $XDG_RUNTIME_DIR/nsm/d/ containing the osc url. This enables discovery of running daemons. New section in the API documentation for the above. Handle write-protected session files and related errors on save. They will not crash the daemon anymore. Fixes and guards against trying to load non-existing sessions and creating new sessions under existing names Handle various crashes-on-exit and replace them with controlled exits. Jackpatch Version 1.0.0 (previously 0.2.0): Jackpatch will finally not "forget" connections anymore. See #gh-74 Reduce verbosity level of log ouput. Document 'hidden' standalone (no NSM) command line mode in --help Handle SIGNALs even when in standalone mode Add a jackpatch desktop file with X-NSM-Capable=true and X-NSM-Exec=jackpatch and NoDisplay=true NSM-Proxy: Add a nsm-proxy desktop file with X-NSM-Capable=true and X-NSM-Exec=nsm-proxy and NoDisplay=true ## 2022-01-15 1.5.3 Add [jackpatch] to terminal log output of jackpatch. Remove hardcoded ANSI colors from terminal log output ## 2021-07-15 1.5.2 pynsm2 library: Fixed a rare crash, where the hostname must be case sensitive but Pythons urlparse forced lower-case. ## 2021-03-19 1.5.1 Web-URLs changed to https://new-session-manager.jackaudio.org and https://github.com/jackaudio/new-session-manager No codechanges. ## 2021-01-15 1.5.0 WARNING: Next scheduled release (2021-04-15) will switch the default session root to $XDG_DATA_HOME ( default on most distributions: ~/.local/share/nsm/ ) With the next release prepare to do one of the following: * Move old sessions to the new root directory (preferred) * Symlink "~/NSM Sessions" to the new root directory * use the nsmd --session-root commandline argument. nsmd: Fix session discovery to not report nested sessions anymore. Also more robust file system error handling. Command line option --quiet: Suppress messages except warnings and errors Protect against orphaned clients or daemons when the server, or even a GUI, crashes. Replace cowboy-slang in info-level OSC with descriptive, technical messages. Legacy-GUI: Fix manpage description and usage with the correct executable name Fix resizing to very small and back. ( / TheGreatWhiteShark ) NSM-Proxy: Multiple layout and style fixes. Better texts for beginners. API: NSM_API_VERSION_PATCH from 0 to 1 (1.1.0 -> 1.1.1) Please see API document chapter "Changes in API Version 1.1.1" Extras: This repository now contains extras (libraries, programs, documentation etc.) Extras are technically not connected to the main programs of this repository. There is no dependency to any "extra" nor any license implications. Please read extras/README.md. nsm.h was moved to extras/nsm.h "extras/pynsm" is now a part of NEW-SM. It was a standalone git repo until now. ## 2020-07-15 1.4.0 Add documentation and manpages. Legacy-GUI: Overhaul look and feel. Rewrite labels and buttons with unambiguous descriptions. Protect text-input dialog windows from empty strings, like "Add New Client" or "New Session" Scale icons, support more icon formats. Show all icons and buttons when attaching to a running nsmd session Various small fixes. Always show correct session name, no matter how the session was loaded or how the GUI was started nsmd: NSM_API_VERSION_MINOR from 0 to 1 (1.0 -> 1.1) Repair nsmd to correctly send client data when running headless and a GUI announces later. ClientId generation now prevent collision with existing IDs. nsmd command line option --load-session to directly load one (Berkelder, Rik) Better detection of clients that failed to launch leads to faster session startup (by 5 seconds) Users get informed by client-label if an executable is not present on the system or permission denied Fixed reply for listing sessions from a plain "Done." to proper reply path with empty string as terminal symbol "/reply", "/nsm/server/list", "" Fix operation reply to last treated client instead to reply to sender (Picot, Mathieu / houston) /nsm/gui/session/name send consistent session name/relative-path pair to the annouced GUI, no matter how the session was loaded. nsm.h :optional-gui: support to nsm.h, for other applications to include and use. (Meyer, Hermann / brummer) ## 2020-06-20 1.3.2 Rename new-session-manager executable to nsm-legacy-gui to prevent future confusion. ## 2020-06-20 1.3.1 Add header copyright even to unchanged files to adhere to stricter packaging requirements. Meson can now switch off individual executables and will stop/error when trying to build without dependencies. ## 2020-06-17 1.3.0 Rebranding to "new session manager" Upstream GUI tools "non-session-manager" and "nsm-proxy" converted to standard FLTK instead of a custom toolkit Changed build system to meson License upgraded to GPLv3 Simplified file structure Fix compiler warnings. ## 2020-04-19 1.2.1 Current state of upstream Non Session Manager v1.2 including unreleased /nsm/gui/session/root ## 2017-07-08 1.2.0 Last release of Non-Session-Manager. Commit 1904aba516341287ac297cefbbcd185f643e5538 ## 2012-03-03 1.1.0 Initial release of Non-Session-Manager. https://non.tuxfamily.org/wiki/2012-03-03%20Release%20Announcement%20v1.1.0 07070100000003000081A40000000000000000000000016258A65C0000894D000000000000000000000000000000000000003700000000new-session-manager-1.6.0+git.20220415.0f6719c/COPYING GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: <program> Copyright (C) <year> <name of author> This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/licenses/why-not-lgpl.html>. 07070100000004000081A40000000000000000000000016258A65C000016AF000000000000000000000000000000000000003900000000new-session-manager-1.6.0+git.20220415.0f6719c/README.md# New Session Manager ## Introduction New Session Manager (NSM) is a tool to assist music production by grouping standalone programs into sessions. Your workflow becomes easy to manage, robust and fast by leveraging the full potential of cooperative applications. NSM continues to be free in every sense of the word: free of cost, free to share and use, free of spyware or ads, free-and-open-source. You can create a session, or project, add programs to it and then use commands to save, start/stop, hide/show all programs at once, or individually. At a later date you can then re-open the session and continue where you left off. All files belonging to the session will be saved in the same directory. If you are a user (and not a programmer or packager) everything you need is to install NSM through your distributions package manager and, highly recommended, Agordejo as a GUI (see below). To learn NSM you don't need to know the background information from our documentation, which is aimed at developers that want to implement NSM support in their programs. Learn the GUI, not the server and protocol. ## Bullet Points * Drop-In replacement for the non-session-manager daemon nsmd and tools (e.g. jackpatch) * Simple and hassle-free build system to make packaging easy * Possibility to react to sensible bug fixes that would not have been integrated original nsmd * Stay upwards and downwards compatible with original nsmd * Conservative and hesitant in regards to new features and behaviour-changes, but possible in principle * Keep the session-manager separate from the other NON* tools Mixer, Sequencer and Timeline. * Protect nsmd from vanishing from the internet one day. * The goal is to become the de-facto standard music session manager for Linux distributions ## User Interface It is highly recommended to use Agordejo ( https://www.laborejo.org/agordejo/ ) as graphical user interface. In fact, if you install Agordejo in your distribution it will install NSM as dependency and you don't need to do anything yourself with this software package. This repository also contains the legacy FLTK interface simply called `nsm-legacy-gui`, symlinked to `non-session-manager` for backwards compatibility. (e.g. autostart scripts etc.) ## Supported Clients While NSM can start and stop any program it only becomes convenient if clients specifically implement support. This enables saving and hiding the GUI, amongst other features. Documentation and tutorials for software-developers will be added at a later date. ## Documentation and Manual Our documentation contains the API specification for the NSM protocol, which is the central document if you want to add NSM support to your own application. You can find html documentation installed to your systems SHARE dir or docs/out/index.html in this repository. There is also an online version https://jackaudio.github.io/new-session-manager/ We also provide a set of manpages for each executable (see Build). ## Fork and License This is a fork of non-session-manager, by Jonathan Moore Liles <male@tuxfamily.net> http://non.tuxfamily.org/ which was released under the GNU GENERAL PUBLIC LICENSE Version 2, June 1991. All files, except nsm.h kept in this fork were GPL "version 2 of the License, or (at your option) any later version." `nsm.h` is licensed under the ISC License. New-Session-Manager changed the license to GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007. See file COPYING Documentation in docs/ is licensed Creative Commons CC-By-Sa. It consist of mostly generated files and snippet files which do not have a license header for technical reasons. All original documentation source files are CC-By-Sa Version v4.0 (see file docs/src/LICENSE), the source file docs/src/api/index.adoc is a derived work from NON-Session-Managers API file which is licensed CC-By-Sa v2.5. Therefore our derived API document is also CC-By-Sa v2.5 (see files docs/src/api/readme.txt and docs/src/api/LICENSE) ## Build The build system is meson. This is a software package that will compile and install multiple executables: * `nsmd`, the daemon or server itself. It is mandatory. * It has no GUI. * Dependency is `liblo`, the OSC library. * `jackpatch`, NSM client to save and remember JACK connections. * It has no GUI. * Dependencies are `JACK Audio Connection Kit` and `liblo`, the OSC library. * Can be deactivated (see below) `-Djackpatch=false` * `nsm-legacy-gui`, Legacy GUI for the user * Formerly known as "non-session-manager" * Dependencies are `FLTK`>=v1.3.0 and `liblo`, the OSC library. * Can be deactivated (see below) `-Dlegacy-gui=false` * `nsm-proxy`, NSM GUI Client to run any program without direct NSM support * Dependencies are `FLTK`>=v1.3.0, `fluid` (FLTK Editor/compiler, maybe in the same package as FLTK, maybe not) and `liblo`, the OSC library. * Can be deactivated (see below) `-Dnsm-proxy=false` ``` meson build --prefix=/usr #or disable individual build targets: #meson build --prefix=/usr -Dlegacy-gui=false -Dnsm-proxy=false -Djackpatch=false cd build && ninja sudo ninja install ``` Optionally you can skip `sudo ninja install` and run all executables from the build-dir. In this case you need to add the build-dir to your PATH environment variable so that the tools can find each other. ## Names of Executable Files and Symlinks Some distributions (and possibly local laws) prevent a forked software project from creating executable files under the same name, if the name itself is an original work subject to copyright, which it arguably is for the "NON-"-suite. Therefore New Session Manager renamed `non-session-manager` to `nsm-legacy-gui`. Installing will also create a symlink to `non-session-manager` for backwards compatibility. (e.g. autostart scripts etc.). 07070100000005000041ED0000000000000000000000046258A65C00000000000000000000000000000000000000000000003400000000new-session-manager-1.6.0+git.20220415.0f6719c/docs07070100000006000081A40000000000000000000000016258A65C00000021000000000000000000000000000000000000003A00000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/CNAMEnew-session-manager.jackaudio.org07070100000007000041ED0000000000000000000000026258A65C00000000000000000000000000000000000000000000003800000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/api07070100000008000081A40000000000000000000000016258A65C000169BF000000000000000000000000000000000000004300000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/api/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Asciidoctor 2.0.17"> <meta name="author" content="Jonathan Moore Liles, Nils Hilbricht"> <title>New Session Manager - API</title> <style> /*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */ /* Uncomment the following line when using as a custom stylesheet */ /* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */ html{font-family:sans-serif;-webkit-text-size-adjust:100%} a{background:none} a:focus{outline:thin dotted} a:active,a:hover{outline:0} h1{font-size:2em;margin:.67em 0} b,strong{font-weight:bold} abbr{font-size:.9em} abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none} dfn{font-style:italic} hr{height:0} mark{background:#ff0;color:#000} code,kbd,pre,samp{font-family:monospace;font-size:1em} pre{white-space:pre-wrap} q{quotes:"\201C" "\201D" "\2018" "\2019"} small{font-size:80%} sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} sup{top:-.5em} sub{bottom:-.25em} img{border:0} svg:not(:root){overflow:hidden} figure{margin:0} audio,video{display:inline-block} audio:not([controls]){display:none;height:0} fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} legend{border:0;padding:0} button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} button,input{line-height:normal} button,select{text-transform:none} button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer} button[disabled],html input[disabled]{cursor:default} input[type=checkbox],input[type=radio]{padding:0} button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} textarea{overflow:auto;vertical-align:top} table{border-collapse:collapse;border-spacing:0} *,::before,::after{box-sizing:border-box} html,body{font-size:100%} body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased} a:hover{cursor:pointer} img,object,embed{max-width:100%;height:auto} object,embed{height:100%} img{-ms-interpolation-mode:bicubic} .left{float:left!important} .right{float:right!important} .text-left{text-align:left!important} .text-right{text-align:right!important} .text-center{text-align:center!important} .text-justify{text-align:justify!important} .hide{display:none} img,object,svg{display:inline-block;vertical-align:middle} textarea{height:auto;min-height:50px} select{width:100%} .subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0} a{color:#2156a5;text-decoration:underline;line-height:inherit} a:hover,a:focus{color:#1d4b8f} a img{border:0} p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} p aside{font-size:.875em;line-height:1.35;font-style:italic} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} h1{font-size:2.125em} h2{font-size:1.6875em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} h4,h5{font-size:1.125em} h6{font-size:1em} hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em} em,i{font-style:italic;line-height:inherit} strong,b{font-weight:bold;line-height:inherit} small{font-size:60%;line-height:inherit} code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} @media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} h1{font-size:2.75em} h2{font-size:2.3125em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} h4{font-size:1.4375em}} table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal} table thead,table tfoot{background:#f7f8f7} table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} table tr.even,table tr.alt{background:#f8f8f7} table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} .center{margin-left:auto;margin-right:auto} .stretch{width:100%} .clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table} .clearfix::after,.float-group::after{clear:both} :not(pre).nobreak{word-wrap:normal} :not(pre).nowrap{white-space:nowrap} :not(pre).pre-wrap{white-space:pre-wrap} :not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed} pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit} pre>code{display:block} pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal} em em{font-style:normal} strong strong{font-weight:400} .keyseq{color:rgba(51,51,51,.8)} kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} .keyseq kbd:first-child{margin-left:0} .keyseq kbd:last-child{margin-right:0} .menuseq,.menuref{color:#000} .menuseq b:not(.caret),.menuref{font-weight:inherit} .menuseq{word-spacing:-.02em} .menuseq b.caret{font-size:1.25em;line-height:.8} .menuseq i.caret{font-weight:bold;text-align:center;width:.45em} b.button::before,b.button::after{position:relative;top:-1px;font-weight:400} b.button::before{content:"[";padding:0 3px 0 2px} b.button::after{content:"]";padding:0 2px 0 3px} p a>code:hover{color:rgba(0,0,0,.9)} #header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} #header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table} #header::after,#content::after,#footnotes::after,#footer::after{clear:both} #content{margin-top:1.25em} #content::before{content:none} #header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} #header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf} #header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px} #header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap} #header .details span:first-child{margin-left:-.125em} #header .details span.email a{color:rgba(0,0,0,.85)} #header .details br{display:none} #header .details br+span::before{content:"\00a0\2013\00a0"} #header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} #header .details br+span#revremark::before{content:"\00a0|\00a0"} #header #revnumber{text-transform:capitalize} #header #revnumber::after{content:"\00a0"} #content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} #toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em} #toc>ul{margin-left:.125em} #toc ul.sectlevel0>li>a{font-style:italic} #toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} #toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} #toc li{line-height:1.3334;margin-top:.3334em} #toc a{text-decoration:none} #toc a:active{text-decoration:underline} #toctitle{color:#7a2518;font-size:1.2em} @media screen and (min-width:768px){#toctitle{font-size:1.375em} body.toc2{padding-left:15em;padding-right:0} #toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} #toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} #toc.toc2>ul{font-size:.9em;margin-bottom:0} #toc.toc2 ul ul{margin-left:0;padding-left:1em} #toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} body.toc2.toc-right{padding-left:0;padding-right:15em} body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}} @media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} #toc.toc2{width:20em} #toc.toc2 #toctitle{font-size:1.375em} #toc.toc2>ul{font-size:.95em} #toc.toc2 ul ul{padding-left:1.25em} body.toc2.toc-right{padding-left:0;padding-right:20em}} #content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px} #content #toc>:first-child{margin-top:0} #content #toc>:last-child{margin-bottom:0} #footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em} #footer-text{color:hsla(0,0%,100%,.8);line-height:1.44} #content{margin-bottom:.625em} .sect1{padding-bottom:.625em} @media screen and (min-width:768px){#content{margin-bottom:1.25em} .sect1{padding-bottom:1.25em}} .sect1:last-child{padding-bottom:0} .sect1+.sect1{border-top:1px solid #e7e7e9} #content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} #content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} #content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} #content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} #content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} details{margin-left:1.25rem} details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent} details>summary::-webkit-details-marker{display:none} details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)} details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)} details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem} .admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} table.tableblock.fit-content>caption.title{white-space:nowrap;width:0} .paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)} .admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} .admonitionblock>table td.icon{text-align:center;width:80px} .admonitionblock>table td.icon img{max-width:none} .admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} .exampleblock>.content>:first-child{margin-top:0} .exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} .sidebarblock>:first-child{margin-top:0} .sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} .exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} .literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8} .literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)} .listingblock>.content{position:relative} .listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5} .listingblock:hover code[data-lang]::before{display:block} .listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5} .listingblock.terminal pre .command:not([data-prompt])::before{content:"$"} .listingblock pre.highlightjs{padding:0} .listingblock pre.highlightjs>code{padding:1em;border-radius:4px} .listingblock pre.prettyprint{border-width:0} .prettyprint{background:#f7f7f8} pre.prettyprint .linenums{line-height:1.45;margin-left:2em} pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0} pre.prettyprint li code[data-lang]::before{opacity:1} pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none} table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none} table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal} table.linenotable td.code{padding-left:.75em} table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} pre.pygments span.linenos{display:inline-block;margin-right:.75em} .quoteblock{margin:0 1em 1.25em 1.5em;display:table} .quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em} .quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} .quoteblock blockquote{margin:0;padding:0;border:0} .quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} .quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} .quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right} .verseblock{margin:0 1em 1.25em} .verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} .verseblock pre strong{font-weight:400} .verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} .quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} .quoteblock .attribution br,.verseblock .attribution br{display:none} .quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} .quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none} .quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0} .quoteblock.abstract{margin:0 1em 1.25em;display:block} .quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center} .quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf} .quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0} .quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem} .quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0} p.tableblock:last-child{margin-bottom:0} td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere} td.tableblock>.content>:last-child{margin-bottom:-1.25em} table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} table.grid-all>*>tr>*{border-width:1px} table.grid-cols>*>tr>*{border-width:0 1px} table.grid-rows>*>tr>*{border-width:1px 0} table.frame-all{border-width:1px} table.frame-ends{border-width:1px 0} table.frame-sides{border-width:0 1px} table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0} table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0} table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0} table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0} table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7} th.halign-left,td.halign-left{text-align:left} th.halign-right,td.halign-right{text-align:right} th.halign-center,td.halign-center{text-align:center} th.valign-top,td.valign-top{vertical-align:top} th.valign-bottom,td.valign-bottom{vertical-align:bottom} th.valign-middle,td.valign-middle{vertical-align:middle} table thead th,table tfoot th{font-weight:bold} tbody tr th{background:#f7f8f7} tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} p.tableblock>code:only-child{background:none;padding:0} p.tableblock{font-size:1em} ol{margin-left:1.75em} ul li ol{margin-left:1.5em} dl dd{margin-left:1.125em} dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none} ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em} ul.unstyled,ol.unstyled{margin-left:0} li>p:empty:only-child::before{content:"";display:inline-block} ul.checklist>li>p:first-child{margin-left:-1em} ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em} ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em} ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em} ul.inline>li{margin-left:1.25em} .unstyled dl dt{font-weight:400;font-style:normal} ol.arabic{list-style-type:decimal} ol.decimal{list-style-type:decimal-leading-zero} ol.loweralpha{list-style-type:lower-alpha} ol.upperalpha{list-style-type:upper-alpha} ol.lowerroman{list-style-type:lower-roman} ol.upperroman{list-style-type:upper-roman} ol.lowergreek{list-style-type:lower-greek} .hdlist>table,.colist>table{border:0;background:none} .hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} td.hdlist1{font-weight:bold;padding-bottom:1.25em} td.hdlist2{word-wrap:anywhere} .literalblock+.colist,.listingblock+.colist{margin-top:-.5em} .colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top} .colist td:not([class]):first-child img{max-width:none} .colist td:not([class]):last-child{padding:.25em 0} .thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd} .imageblock.left{margin:.25em .625em 1.25em 0} .imageblock.right{margin:.25em 0 1.25em .625em} .imageblock>.title{margin-bottom:0} .imageblock.thumb,.imageblock.th{border-width:6px} .imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} .image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} .image.left{margin-right:.625em} .image.right{margin-left:.625em} a.image{text-decoration:none;display:inline-block} a.image object{pointer-events:none} sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} sup.footnote a,sup.footnoteref a{text-decoration:none} sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline} #footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} #footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0} #footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em} #footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em} #footnotes .footnote:last-of-type{margin-bottom:0} #content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} div.unbreakable{page-break-inside:avoid} .big{font-size:larger} .small{font-size:smaller} .underline{text-decoration:underline} .overline{text-decoration:overline} .line-through{text-decoration:line-through} .aqua{color:#00bfbf} .aqua-background{background:#00fafa} .black{color:#000} .black-background{background:#000} .blue{color:#0000bf} .blue-background{background:#0000fa} .fuchsia{color:#bf00bf} .fuchsia-background{background:#fa00fa} .gray{color:#606060} .gray-background{background:#7d7d7d} .green{color:#006000} .green-background{background:#007d00} .lime{color:#00bf00} .lime-background{background:#00fa00} .maroon{color:#600000} .maroon-background{background:#7d0000} .navy{color:#000060} .navy-background{background:#00007d} .olive{color:#606000} .olive-background{background:#7d7d00} .purple{color:#600060} .purple-background{background:#7d007d} .red{color:#bf0000} .red-background{background:#fa0000} .silver{color:#909090} .silver-background{background:#bcbcbc} .teal{color:#006060} .teal-background{background:#007d7d} .white{color:#bfbfbf} .white-background{background:#fafafa} .yellow{color:#bfbf00} .yellow-background{background:#fafa00} span.icon>.fa{cursor:default} a span.icon>.fa{cursor:inherit} .admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} .admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c} .admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} .admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900} .admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400} .admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000} .conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} .conum[data-value] *{color:#fff!important} .conum[data-value]+b{display:none} .conum[data-value]::after{content:attr(data-value)} pre .conum[data-value]{position:relative;top:-.125em} b.conum *{color:inherit!important} .conum:not([data-value]):empty{display:none} dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} .print-only{display:none!important} @page{margin:1.25cm .75cm} @media print{*{box-shadow:none!important;text-shadow:none!important} html{font-size:80%} a{color:inherit!important;text-decoration:underline!important} a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} abbr[title]{border-bottom:1px dotted} abbr[title]::after{content:" (" attr(title) ")"} pre,blockquote,tr,img,object,svg{page-break-inside:avoid} thead{display:table-header-group} svg{max-width:100%} p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} #header,#content,#footnotes,#footer{max-width:none} #toc,.sidebarblock,.exampleblock>.content{background:none!important} #toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important} body.book #header{text-align:center} body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em} body.book #header .details{border:0!important;display:block;padding:0!important} body.book #header .details span:first-child{margin-left:0!important} body.book #header .details br{display:block} body.book #header .details br+span::before{content:none!important} body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} .listingblock code[data-lang]::before{display:block} #footer{padding:0 .9375em} .hide-on-print{display:none!important} .print-only{display:block!important} .hide-for-print{display:none!important} .show-for-print{display:inherit!important}} @media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem} .sect1{padding:0!important} .sect1+.sect1{border:0} #footer{background:none} #footer-text{color:rgba(0,0,0,.6);font-size:.9em}} @media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}} </style> </head> <body class="article"> <div id="header"> <h1>New Session Manager - API</h1> <div class="details"> <span id="author" class="author">Jonathan Moore Liles, Nils Hilbricht</span><br> <span id="revnumber">version API 1.1.2</span> <br><span id="revremark">License CC-By-SA v2.5</span> </div> <div id="toc" class="toc"> <div id="toctitle">Table of Contents</div> <ul class="sectlevel1"> <li><a href="#_client_behavior_under_session_management">1. Client Behavior Under Session Management</a> <ul class="sectlevel2"> <li><a href="#_file_menu">1.1. File Menu</a> <ul class="sectlevel3"> <li><a href="#_new">1.1.1. New</a></li> <li><a href="#_open">1.1.2. Open</a></li> <li><a href="#_save">1.1.3. Save</a></li> <li><a href="#_save_as">1.1.4. Save As</a></li> <li><a href="#_close_as_distinguished_from_quit_or_exit">1.1.5. Close (as distinguished from Quit or Exit)</a></li> <li><a href="#_quit_or_exit">1.1.6. Quit or Exit</a></li> </ul> </li> <li><a href="#_data_storage">1.2. Data Storage</a> <ul class="sectlevel3"> <li><a href="#_internal_files">1.2.1. Internal Files</a></li> <li><a href="#_external_files">1.2.2. External Files</a></li> <li><a href="#_session_root_and_session_directories">1.2.3. Session Root and Session Directories</a> <ul class="sectlevel4"> <li><a href="#_subdirectories_hierarchical_structure">1.2.3.1. Subdirectories / Hierarchical Structure</a></li> <li><a href="#_write_protection_for_session_templates">1.2.3.2. Write-Protection for Session Templates</a></li> <li><a href="#_lockfiles">1.2.3.3. Lockfiles</a></li> <li><a href="#_daemon_discovery">1.2.3.4. Daemon Discovery</a></li> </ul> </li> </ul> </li> </ul> </li> <li><a href="#_nsm_osc_protocol">2. NSM OSC Protocol</a> <ul class="sectlevel2"> <li><a href="#_establishing_a_connection">2.1. Establishing a Connection</a> <ul class="sectlevel3"> <li><a href="#_announce">2.1.1. Announce</a></li> <li><a href="#_response">2.1.2. Response</a></li> </ul> </li> <li><a href="#_server_to_client_control_messages">2.2. Server to Client Control Messages</a> <ul class="sectlevel3"> <li><a href="#_quit">2.2.1. Quit</a></li> <li><a href="#server-to-client-control-messages-open">2.2.2. Open</a> <ul class="sectlevel4"> <li><a href="#_response_2">2.2.2.1. Response</a></li> </ul> </li> <li><a href="#_save_2">2.2.3. Save</a> <ul class="sectlevel4"> <li><a href="#_response_3">2.2.3.1. Response</a></li> </ul> </li> </ul> </li> <li><a href="#_server_to_client_informational_messages">2.3. Server to Client Informational Messages</a> <ul class="sectlevel3"> <li><a href="#_session_is_loaded">2.3.1. Session is Loaded</a></li> <li><a href="#_show_optional_gui">2.3.2. Show Optional Gui</a></li> </ul> </li> <li><a href="#_client_to_server_informational_messages">2.4. Client to Server Informational Messages</a> <ul class="sectlevel3"> <li><a href="#_optional_gui">2.4.1. Optional GUI</a></li> <li><a href="#_progress">2.4.2. Progress</a></li> <li><a href="#_dirtiness">2.4.3. Dirtiness</a></li> <li><a href="#_status_messsages">2.4.4. Status Messsages</a></li> </ul> </li> <li><a href="#_error_code_definitions">2.5. Error Code Definitions</a></li> <li><a href="#_client_to_server_control">2.6. Client to Server Control</a></li> <li><a href="#_server_control_api">2.7. Server Control API</a> <ul class="sectlevel3"> <li><a href="#_client_to_client_communication">2.7.1. Client to Client Communication</a></li> </ul> </li> </ul> </li> <li><a href="#_api_versions_and_behaviour_changes">3. API Versions and Behaviour Changes</a> <ul class="sectlevel2"> <li><a href="#_guidelines">3.1. Guidelines</a></li> <li><a href="#_changes_in_api_version_1_1_0">3.2. Changes in API Version 1.1.0</a></li> <li><a href="#_changes_in_api_version_1_1_1">3.3. Changes in API Version 1.1.1</a></li> <li><a href="#_changes_in_api_version_1_1_2">3.4. Changes in API Version 1.1.2</a></li> </ul> </li> </ul> </div> </div> <div id="content"> <div id="preamble"> <div class="sectionbody"> <div class="admonitionblock important"> <table> <tr> <td class="icon"> <div class="title">Important</div> </td> <td class="content"> "New Session Manager" is a community version of the <a href="http://non.tuxfamily.org/nsm/API.html">"Non Session Manager" by Jonathan Moore Liles</a>, who also wrote the majority of this API document, especially the API itself. <strong>The API is the same</strong>. Any technical changes or differences in behaviour are described in <a href="#_api_versions_and_behaviour_changes">API Versions and Behaviour Changes</a>. All other changes to this document can be reviewed by accessing the git log. This document is licensed under CC-By-Sa v2.5. See <a href="https://github.com/jackaudio/new-session-manager/tree/master/docs/src/api">LICENSE</a> </td> </tr> </table> </div> <div class="admonitionblock important"> <table> <tr> <td class="icon"> <div class="title">Important</div> </td> <td class="content"> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in <a href="https://tools.ietf.org/html/rfc2119">RFC 2119</a>. </td> </tr> </table> </div> <div class="paragraph"> <p>The "New Session Manager"-API is used by many music and audio programs in Linux distributions to allow any number of independent programs to be managed together as part of a logical session (i.e. a song). Thus, operations such as loading and saving are synchronized.</p> </div> <div class="paragraph"> <p>The API comprises a simple Open Sound Control (OSC) based protocol, along with some behavioral guidelines, which can easily be implemented by various applications.</p> </div> <div class="paragraph"> <p>This project contains a program called <code>nsmd</code> which is an implementation of the server side of the NSM API. <code>nsmd</code> can be controlled by direct OSC messages, or (more commonly) a GUI: Included in this package is the <code>nsm-legacy-gui</code>, which gets symlinked to "non-session-manager`. Another GUI is "Agordejo". Other applications exist that (partially) support the NSM API and are able to load clients, but they do not use the New-Session-Manager (or Non-Session-Manager) implementation and are therefore out of scope for this document.</p> </div> <div class="paragraph"> <p>However, the same server-side API can also be implemented by other programs (such as Carla), although consistency and robustness will likely suffer if non-NSM compliant clients are allowed to participate in a session.</p> </div> <div class="paragraph"> <p>There is no direct dependency for client implementations, as long as they can send and receive OSC. Some clients use <code>liblo</code> (the OSC library), which becomes a dependency if you choose to implement NSM-support with the provided header file <code>nsm.h</code> (<code>extras/nsm.h/nsm.h</code> in the git repository). Some clients use the provided single-file python library <code>pynsm</code> (<code>extras/pynsm/nsmclient.py</code> in the git repository) which has no dependencies outside the Python3 standard library.</p> </div> <div class="paragraph"> <p>The aim of this project is to thoroughly define the behavior required of clients. Often the difficulty with other session-management approaches has been not in implementing code-support for them, but in not defining rules and behaviour clearly enough.</p> </div> <div class="paragraph"> <p>As written above unambiguous rules are created by using RFC 2119 in this document. For the good of the user, these rules are meant to be followed and are non-negotiable. If an application does not conform to this specification it should be considered broken. Consistency across applications under session management is very important for a good user experience.</p> </div> </div> </div> <div class="sect1"> <h2 id="_client_behavior_under_session_management">1. Client Behavior Under Session Management</h2> <div class="sectionbody"> <div class="paragraph"> <p>Most graphical applications make available to the user a common set of file operations, typically presented under a File or Project menu.</p> </div> <div class="paragraph"> <p>These are: New, Open, Save, Save As, Close and Quit or Exit.</p> </div> <div class="paragraph"> <p>The following sub-sections describe how these options should behave when the application is part of an NSM session. These rules only apply when session management is active, that is, after the <code>announce</code> handshake described in the <a href="#_nsm_osc_protocol">NSM OSC Protocol</a> section. In order to provide a consistent and predictable user experience, it is critically important for applications to adhere to these guidelines.</p> </div> <div class="sect2"> <h3 id="_file_menu">1.1. File Menu</h3> <div class="sect3"> <h4 id="_new">1.1.1. New</h4> <div class="paragraph"> <p>This option MAY empty/reset the current file or project (possibly after user confirmation). It MUST NOT allow the user to create a new project/file in another location.</p> </div> </div> <div class="sect3"> <h4 id="_open">1.1.2. Open</h4> <div class="paragraph"> <p>This option MUST be disabled.</p> </div> <div class="paragraph"> <p>The application MAY elect to implement an option called "Import into Session", which creates a copy of a file/project which is then saved at the session path provided by NSM.</p> </div> </div> <div class="sect3"> <h4 id="_save">1.1.3. Save</h4> <div class="paragraph"> <p>This option should behave as normal, saving the current file/project as established by the NSM <code>open</code> message.</p> </div> <div class="paragraph"> <p>This option MUST NOT present the user with a choice of where to save the file.</p> </div> </div> <div class="sect3"> <h4 id="_save_as">1.1.4. Save As</h4> <div class="paragraph"> <p>This option MUST be disabled.</p> </div> <div class="paragraph"> <p>The application MAY elect to implement an option called 'Export from Session', which creates a copy of the current file/project which is then saved in a user-specified location outside of the session path provided by NSM.</p> </div> </div> <div class="sect3"> <h4 id="_close_as_distinguished_from_quit_or_exit">1.1.5. Close (as distinguished from Quit or Exit)</h4> <div class="paragraph"> <p>This option MUST be disabled unless its meaning is to disconnect the application from session management.</p> </div> </div> <div class="sect3"> <h4 id="_quit_or_exit">1.1.6. Quit or Exit</h4> <div class="paragraph"> <p>This option MAY behave as normal (possibly asking the user to confirm exiting), or MAY do nothing to only allow quit from the session-manager control. When the client supports :optional-gui: this option SHOULD be replaced with hiding the client’s GUI so a quit by window manager hides.</p> </div> </div> </div> <div class="sect2"> <h3 id="_data_storage">1.2. Data Storage</h3> <div class="sect3"> <h4 id="_internal_files">1.2.1. Internal Files</h4> <div class="paragraph"> <p>All project specific data created by a client MUST be stored in the per-client storage area provided by NSM. This includes all recorded audio and MIDI files, snapshots, etc. Only global configuration items, exports, and renders of the project may be stored elsewhere (wherever the user specifies).</p> </div> </div> <div class="sect3"> <h4 id="_external_files">1.2.2. External Files</h4> <div class="paragraph"> <p>Files required by the project but external to it (typically read-only data such as audio samples) SHOULD be referenced by creating a symbolic link within the assigned session area, and then referring to the symlink. This allows sessions to be archived and transported simply (e.g. with "tar -h") by tools that have no knowledge of the project formats of the various clients in the session. The symlinks thus created should, at the very least, be named after the files they refer to. Some unique component may be required to prevent collisions.</p> </div> </div> <div class="sect3"> <h4 id="_session_root_and_session_directories">1.2.3. Session Root and Session Directories</h4> <div class="paragraph"> <p>Client programs MUST NOT handle the following themselves. This section is background-information.</p> </div> <div class="paragraph"> <p>NSM follows the <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG Base Directory Specifications</a></p> </div> <div class="paragraph"> <p>All existing and new sessions are directories below the session-root, which defaults to <code>$XDG_DATA_HOME/nsm/</code>, which usually results in <code>$HOME/.local/share/nsm/</code>.</p> </div> <div class="paragraph"> <p>Each session directory contains a file <code>session.nsm</code> with one client per line <code>name:executable:UID\n</code> For example:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight"><code>JACKPatch:jackpatch:nBEIQ jack_mixer:jack_mixer:nTXHV Carla-Rack:carla-rack:nFAOD</code></pre> </div> </div> <div class="paragraph"> <p><code>nsmd</code> loads and saves this file, client names are their self-reported names. The file format is final and frozen. Additions or changes SHALL NOT be made.</p> </div> <div class="sect4"> <h5 id="_subdirectories_hierarchical_structure">1.2.3.1. Subdirectories / Hierarchical Structure</h5> <div class="paragraph"> <p>Subdirectories MAY be made to organize sessions into meaningful structures, such as album/track or composer/genre/piece. For example: <code>Johann Sebastian Bach/Kantaten/Wie schön leuchtet der Morgenstern</code>. Which results in the same directory structure on disk. Session names can contain any characters that are supported by the underlying file system, usually UTF-8.</p> </div> <div class="paragraph"> <p>Subdirectories are created by either <code>nsmd</code> itself or by the users themselves, through their file manager or a GUI (while the session is not open).</p> </div> <div class="paragraph"> <p>The project_name from <code>/nsm/server/new s:project_name</code> accepts the format <code>a/b/c/d</code>.</p> </div> <div class="paragraph"> <p>Any session itself MUST be a "leaf" in this directory tree. A session MUST NOT contain further session subdirectories: any directory that contains a file <code>session.nsm</code> is the final element in the hierarchy.</p> </div> </div> <div class="sect4"> <h5 id="_write_protection_for_session_templates">1.2.3.2. Write-Protection for Session Templates</h5> <div class="paragraph"> <p>Write protection for a whole session directory can either happen by "accident" (files from another user, a network mount etc.) or on purpose, to protect a session template against accidental changes. The latter is possible with a recursive <code>chown</code>, <code>chmod</code> or <code>chattr -R +i session-dir</code>.</p> </div> <div class="paragraph"> <p>nsmd itself just checks if <code>session.nsm</code> is read-only. In this case it will not send the save command to it’s session clients. This does not prevent hypothetical problems when the user triggers a clients internal save command in a write protected directory. Clients SHOULD handle their write protected save files themselves.</p> </div> <div class="paragraph"> <p>Advanced contraptions, like overlay filesystems or copy-on-write hardlinks to create read-only sessions without the clients noticing, are out of scope for nsm.</p> </div> </div> <div class="sect4"> <h5 id="_lockfiles">1.2.3.3. Lockfiles</h5> <div class="paragraph"> <p>Because multiple <code>nsmd</code> can run at the same time we need to prevent accidental write-access to the same session by different nsm-daemons, and subsequently GUIs.</p> </div> <div class="paragraph"> <p>Therefore each currently open session creates a lockfile under <code>$XDG_RUNTIME_DIR/nsm/</code> (usually <code>/run/user/XXXX/nsm/</code>) that tells <code>nsmd</code> to not open such a locked session. This directory gets cleaned by the operating system, preventing sessions to stay locked after e.g. a power failure.</p> </div> <div class="paragraph"> <p>The lockfile is named after the simple session name combined with a numeric ID for the session root. It is possible that two <code>nsmd</code> opened two different session roots, both with the same simple session name, e.g. "my song". Lockfiles are able to distinguish between those and will not prevent access in this scenario. The numeric ID is a djb2 hash modulo (%) 65521 of the session root directory (see <code>src/file.cpp</code> function <code>simple_hash()</code>).</p> </div> <div class="paragraph"> <p>The lockfile contains, on separate lines:</p> </div> <div class="ulist"> <ul> <li> <p>The absolute path to the session, including the root-dir, which could be overriden by <code>nsmd --session-root</code>, allowing two sessions of the same basic name in different roots.</p> </li> <li> <p>the OSC URL of the server that runs this session, the same as <code>$NSM_URL</code>.</p> </li> <li> <p>the PID of <code>nsmd</code></p> </li> </ul> </div> <div class="paragraph"> <p>Example:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight"><code>/home/johann/.local/share/nsm/cantatas/easter1751 osc.udp://myuser.localdomain:11287/ 3022</code></pre> </div> </div> </div> <div class="sect4"> <h5 id="_daemon_discovery">1.2.3.4. Daemon Discovery</h5> <div class="paragraph"> <p>Each running <code>nsmd</code>, per user, creates a state file under <code>$XDG_RUNTIME_DIR/nsm/d/</code> (usually <code>/run/user/XXXX/nsm/d/</code>) that can be used to look up running daemons, even if no session is loaded. The name of the file is <code>nsmd</code> PID and the files contain their daemons osc.udp URL that is compatible with the --nsm-url parameter of the GUI.</p> </div> <div class="paragraph"> <p>This enables you to e.g. start nsmd at boot with a random free port. Server-control programs such as GUIs can then use this to look for running servers without requiring the user to look up and input an osc URL manually as command line parameter.</p> </div> </div> </div> </div> </div> </div> <div class="sect1"> <h2 id="_nsm_osc_protocol">2. NSM OSC Protocol</h2> <div class="sectionbody"> <div class="paragraph"> <p>All message parameters are REQUIRED. All messages MUST be sent from the same socket as the <code>announce</code> message, using the <code>lo_send_from</code> method of liblo or its equivalent, as the server uses the return addresses to distinguish between clients.</p> </div> <div class="paragraph"> <p>Clients MUST create thier OSC servers using the same protocol (UDP,TCP) as found in <code>NSM_URL</code>. <code>nsmd</code> itself is using UDP only.</p> </div> <div class="sect2"> <h3 id="_establishing_a_connection">2.1. Establishing a Connection</h3> <div class="sect3"> <h4 id="_announce">2.1.1. Announce</h4> <div class="paragraph"> <p>At launch, the client MUST check the environment for the value of <code>NSM_URL</code>. If present, the client MUST send the following message to the provided address as soon as it is ready to respond to the <code>/nsm/client/open</code> event:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/server/announce s:application_name s:capabilities s:executable_name i:api_version_major i:api_version_minor i:pid</code></pre> </div> </div> <div class="paragraph"> <p>If <code>NSM_URL</code> is undefined, invalid, or unreachable, then the client should proceed assuming that session management is unavailable.</p> </div> <div class="paragraph"> <p><code>api_version_major</code> and <code>api_version_minor</code> must be the two parts of the version number of the NSM API as defined by this document.</p> </div> <div class="paragraph"> <p>Note that if the application intends to register JACK clients, <code>application_name</code> MUST be the same as the name that would normally be passed to <code>jack_client_open</code>. For example, Non-Mixer sends "Non-Mixer" as its <code>application_name</code>. Applications MUST NOT register their JACK clients until receiving an <code>open</code> message; the <code>open</code> message will provide a unique client name prefix suitable for passing to JACK. This is probably the most complex requirement of the NSM API, but it isn’t difficult to implement, especially if the application simply wishes to delay its initialization process briefly while awaiting the <code>announce</code> reply and subsequent <code>open</code> message.</p> </div> <div class="paragraph"> <p><code>capabilities</code> MUST be a string containing a colon separated list of the special capabilities the client possesses. e.g. <code>:dirty:switch:progress:</code></p> </div> <div class="paragraph"> <p><code>executable_name</code> MUST be the executable name that the program was launched with. For C programs, this is simply the value of <code>argv[0]</code>. Note that hardcoding the name of the program here is not the same as using, as the user may have launched the program from a script with a different name using exec, or have created a symlink to the program. Getting the correct value in scripting languages like Python can be more challenging.</p> </div> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 1. Available Client Capabilities</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Name</th> <th class="tableblock halign-left valign-top">Description</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">switch</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">client is capable of responding to multiple <code>open</code> messages without restarting</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">dirty</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">client knows when it has unsaved changes</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">progress</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">client can send progress updates during time-consuming operations</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">message</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">client can send textual status updates</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">optional-gui</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">client has an optional GUI</p></td> </tr> </tbody> </table> </div> <div class="sect3"> <h4 id="_response">2.1.2. Response</h4> <div class="paragraph"> <p>The server will respond to the client’s announce message with the following message:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/reply "/nsm/server/announce" s:message s:name_of_session_manager s:capabilities</code></pre> </div> </div> <div class="paragraph"> <p><code>message</code> is a welcome message.</p> </div> <div class="paragraph"> <p>The value of <code>name_of_session_manager</code> will depend on the implementation of the NSM server. It might say "New Session Manager", or it might say "Non Session Manager" etc. This is for display to the user.</p> </div> <div class="paragraph"> <p><code>capabilities</code> will be a string containing a colon separated list of special server capabilities.</p> </div> <div class="paragraph"> <p>Presently, the server <code>capabilities</code> are:</p> </div> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 2. Available Server Capabilities</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Name</th> <th class="tableblock halign-left valign-top">Description</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">server-control</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">client-to-server control</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">broadcast</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">server responds to /nsm/server/broadcast message</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">optional-gui</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">server responds to optional-gui messages. This capability is always present and MUST be supported by any server implementation.</p></td> </tr> </tbody> </table> <div class="paragraph"> <p>A client should not consider itself to be under session management until it receives this response. For example, the Non applications activate their "SM" blinkers at this time.</p> </div> <div class="paragraph"> <p>If there is an error, a reply of the following form will be sent to the client:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/error "/nsm/server/announce" i:error_code s:error_message</code></pre> </div> </div> <div class="paragraph"> <p>The following table defines possible values of <code>error_code</code>:</p> </div> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 3. Response codes</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Code</th> <th class="tableblock halign-left valign-top">Meaning</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_GENERAL</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">General Error</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_INCOMPATIBLE_API</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Incompatible API version</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_BLACKLISTED</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Client has been blacklisted.</p></td> </tr> </tbody> </table> </div> </div> <div class="sect2"> <h3 id="_server_to_client_control_messages">2.2. Server to Client Control Messages</h3> <div class="paragraph"> <p>Compliant clients MUST accept the client control messages described in this section. All client control messages REQUIRE a response. Responses MUST be delivered back to the sender (<code>nsmd</code>) from the same socket used by the client in its <code>announce</code> message (by using <code>lo_send_from</code>) AFTER the action has been completed or if an error is encountered. The required response is described in the subsection for each message.</p> </div> <div class="paragraph"> <p>If there is an error and the action cannot be completed, then <code>error_code</code> MUST be set to a valid error code (see <a href="#_error_code_definitions">Error Code Definitions</a>) and <code>message</code> to a string describing the problem (suitable for display to the user).</p> </div> <div class="paragraph"> <p>The reply can take one of the following two forms, where path MUST be the <code>path</code> of the message being replied to (e.g. "nsm/client/save":</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/reply s:path s:message</code></pre> </div> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/error s:path i:error_code s:message</code></pre> </div> </div> <div class="sect3"> <h4 id="_quit">2.2.1. Quit</h4> <div class="paragraph"> <p>There is no message for this. Clients will receive the Unix SIGTERM signal and MUST close cleanly IMMEDIATELY, without displaying any kind of dialog to the user and regardless of whether or not unsaved changes would be lost. When a session is closed the application will receive this signal soon after having responded to a <code>save</code> message.</p> </div> </div> <div class="sect3"> <h4 id="server-to-client-control-messages-open">2.2.2. Open</h4> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/open s:path_to_instance_specific_project s:display_name s:client_id</code></pre> </div> </div> <div class="paragraph"> <p><code>path_to_instance_specific_project</code> is a path name in the form client_name.ID, assigned to the client for storing its project data. The client MUST choose one of the four strategies below to save, so that every file in the session can be traced back to a client and, vice versa, a client name.ID can be used to look up all its files. (For example to clean up the session dir)</p> </div> <div class="ulist"> <ul> <li> <p>The client has no state and does not save at all</p> <div class="ulist"> <ul> <li> <p>and it MUST NOT misuse e.g. ~/.config to save session specific information e.g. synth-instrument settings</p> </li> </ul> </div> </li> <li> <p>The client may use the path client_name.ID directly, resulting in a file client_name.ID in the session directory</p> </li> <li> <p>The client may append its native file extension (e.g. <code>.json</code>) to the path client_name.ID</p> </li> <li> <p>The client may use the path as directory, creating arbitrary files below, for example recorded .wav.</p> <div class="ulist"> <ul> <li> <p>and it MUST NOT use the client ID below this point. This way the data stays transferable by hand to another client instance (in another session).</p> </li> <li> <p>best case practice is to always use the same file names, for example <code>client_name.ID/savefile.json</code></p> </li> </ul> </div> </li> </ul> </div> <div class="paragraph"> <p>If a project exists at the path, the client MUST immediately open it.</p> </div> <div class="paragraph"> <p>If a project does not exist at the path, then the client MUST immediately create and open a new one at the specified path or, for clients which hold all their state in memory, store the path for later use when responding to the <code>save</code> message.</p> </div> <div class="paragraph"> <p>No file or directory will be created at the specified path by the server. It is up to the client to create what it needs.</p> </div> <div class="paragraph"> <p>For clients which HAVE NOT specified the <code>:switch:</code> capability, the <code>open</code> message will only be delivered once, immediately following the <code>announce</code> response.</p> </div> <div class="paragraph"> <p>For clients which HAVE specified the <code>:switch:</code> capability, the client MUST immediately switch to the specified project or create a new one if it doesn’t exist.</p> </div> <div class="paragraph"> <p>Clients which are incapable of switching projects or are prone to crashing upon switching MUST NOT include <code>:switch:</code> in their capability string.</p> </div> <div class="paragraph"> <p>If the user the is allowed to run two or more instances of the application simultaneously then such an application MUST PRE-PEND the provided <code>client_id</code> string, followed by "/", to any names it registers with common subsystems (e.g. JACK client names). This ensures that multiple instances of the same application can be restored in any order without scrambling the JACK connections or causing other conflicts.</p> </div> <div class="paragraph"> <p>The provided <code>client_id</code> will be a concatenation of the value of <code>application_name</code> sent by the client in its <code>announce</code> message and a unique identifier.</p> </div> <div class="paragraph"> <p>Therefore, applications which create single JACK clients can use the value of <code>client_id</code> directly as their JACK client name.</p> </div> <div class="paragraph"> <p>Applications which register multiple JACK clients (e.g. Carla or Non-Mixer) MUST PRE-PEND <code>client_id</code> value, followed by "/", to the client names they register with JACK and the application determined part MUST be unique for that (JACK) client.</p> </div> <div class="paragraph"> <p>For example, Carla is a plugin-host that loads each plugin as JACK client. Suitable JACK client names are: <code>carla-jack-multi.nBAF/ZynAddSubFx</code> or <code>carla-jack-multi.nBAF/Helm</code> Please note that ZynAddSubFx and Helm are <strong>not ports</strong> but clients. Each of them can have any number of audio and midi ports below them.</p> </div> <div class="paragraph"> <p>Note that this means that the application MUST NOT register with JACK (or any other subsystem requiring unique names) until it receives an <code>open</code> message from NSM. Likewise, applications with the <code>:switch:</code> capability should close their JACK clients and re-create them with using the new <code>client_id</code> (renaming JACK-clients is not possible, only ports).</p> </div> <div class="paragraph"> <p>A response is REQUIRED as soon as the open operation has been completed. Ongoing progress MAY be indicated by sending messages to <code>/nsm/client/progress</code>.</p> </div> <div class="sect4"> <h5 id="_response_2">2.2.2.1. Response</h5> <div class="paragraph"> <p>The client MUST respond to the 'open' message with:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/reply "/nsm/client/open" s:message</code></pre> </div> </div> <div class="paragraph"> <p>Or</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/error "/nsm/client/open" i:error_code s:message</code></pre> </div> </div> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 4. Response codes</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Code</th> <th class="tableblock halign-left valign-top">Meaning</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">General Error</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_BAD_PROJECT</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">An existing project file was found to be corrupt</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_CREATE_FAILED</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">A new project could not be created</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_UNSAVED_CHANGES</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Unsaved changes would be lost</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_NOT_NOW</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Operation cannot be completed at this time</p></td> </tr> </tbody> </table> </div> </div> <div class="sect3"> <h4 id="_save_2">2.2.3. Save</h4> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/save</code></pre> </div> </div> <div class="paragraph"> <p>This message will only be delivered after a previous <code>open</code> message, and may be sent any number of times within the course of a session (including zero, if the user aborts the session).</p> </div> <div class="sect4"> <h5 id="_response_3">2.2.3.1. Response</h5> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/reply "/nsm/client/save" s:message</code></pre> </div> </div> <div class="paragraph"> <p>Or</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/error "/nsm/client/save" i:error_code s:message</code></pre> </div> </div> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 5. Response codes</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Code</th> <th class="tableblock halign-left valign-top">Meaning</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">General Error</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_SAVE_FAILED</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Project could not be saved</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_NOT_NOW</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Operation cannot be completed at this time</p></td> </tr> </tbody> </table> </div> </div> </div> <div class="sect2"> <h3 id="_server_to_client_informational_messages">2.3. Server to Client Informational Messages</h3> <div class="sect3"> <h4 id="_session_is_loaded">2.3.1. Session is Loaded</h4> <div class="paragraph"> <p>Accepting this message is optional. The intent is to signal to clients which may have some interdependence (say, peer to peer OSC connections) that the session is fully loaded and all their peers are available. Most clients will not need to act on this message. This message has no meaning when a session is being built or run; only when it is initially loaded. Clients who intend to act on this message MUST NOT do so by delaying initialization waiting for it.</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/session_is_loaded</code></pre> </div> </div> <div class="paragraph"> <p>This message does not require a response.</p> </div> </div> <div class="sect3"> <h4 id="_show_optional_gui">2.3.2. Show Optional Gui</h4> <div class="paragraph"> <p>If the client has specified the <code>optional-gui</code> capability, then it may receive this message from the server when the user wishes to change the visibility state of the GUI. It doesn’t matter if the optional GUI is integrated with the program or if it is a separate program \(as is the case with SooperLooper\). When the GUI is hidden, there should be no window mapped and if the GUI is a separate program, it should be killed.</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/show_optional_gui</code></pre> </div> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/hide_optional_gui</code></pre> </div> </div> <div class="paragraph"> <p>This message does not require a response.</p> </div> </div> </div> <div class="sect2"> <h3 id="_client_to_server_informational_messages">2.4. Client to Server Informational Messages</h3> <div class="sect3"> <h4 id="_optional_gui">2.4.1. Optional GUI</h4> <div class="paragraph"> <p>If the client has specified the <code>optional-gui</code> capability, then it MUST send this message whenever the state of visibility of the optional GUI has changed. It also MUST send this message after its announce message to indicate the initial visibility state of the optional GUI.</p> </div> <div class="paragraph"> <p>The client SHOULD always start hidden, if not saved as visible. That implies the first load, after adding to the session, SHOULD always be hidden.</p> </div> <div class="paragraph"> <p>It is the responsibility of the client to remember the visibility state of its GUI across session loads.</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/gui_is_hidden</code></pre> </div> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/gui_is_shown</code></pre> </div> </div> <div class="paragraph"> <p>No response will be delivered.</p> </div> </div> <div class="sect3"> <h4 id="_progress">2.4.2. Progress</h4> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/progress f:progress</code></pre> </div> </div> <div class="paragraph"> <p>For potentially time-consuming operations, such as <code>save</code> and <code>open</code>, progress updates may be indicated throughout the duration by sending a floating point value between 0.0 and 1.0, 1.0 indicating completion, to the NSM server.</p> </div> <div class="paragraph"> <p>The server will not send a response to these messages, but will relay the information to the user.</p> </div> <div class="paragraph"> <p>Note that even when using the <code>progress</code> feature, the final response to the <code>save</code> or <code>open</code> message is still REQUIRED.</p> </div> <div class="paragraph"> <p>Clients which intend to send progress messages MUST include <code>:progress:</code> in their <code>announce</code> capability string.</p> </div> </div> <div class="sect3"> <h4 id="_dirtiness">2.4.3. Dirtiness</h4> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/is_dirty</code></pre> </div> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/is_clean</code></pre> </div> </div> <div class="paragraph"> <p>Some clients may be able to inform the server when they have unsaved changes pending. Such clients may optionally send <code>is_dirty</code> and <code>is_clean</code> messages.</p> </div> <div class="paragraph"> <p>Clients which have and use this capability MUST include <code>:dirty:</code> in their <code>announce</code> capability string.</p> </div> </div> <div class="sect3"> <h4 id="_status_messsages">2.4.4. Status Messsages</h4> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/client/message i:priority s:message</code></pre> </div> </div> <div class="paragraph"> <p>Clients may send miscellaneous status updates to the server for possible display to the user. This may simply be chatter that is normally written to the console. <code>priority</code> MUST be a number from 0 to 3, 3 being the most important.</p> </div> <div class="paragraph"> <p>Clients which have and use this capability MUST include <code>:message:</code> in their <code>announce</code> capability string.</p> </div> </div> </div> <div class="sect2"> <h3 id="_error_code_definitions">2.5. Error Code Definitions</h3> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 6. Error Code Definitions</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Symbolic Name</th> <th class="tableblock halign-left valign-top">Integer Value</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_GENERAL</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-1</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_INCOMPATIBLE_API</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-2</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_BLACKLISTED</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-3</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_LAUNCH_FAILED</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-4</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_NO_SUCH_FILE</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-5</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_NO_SESSION_OPEN</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-6</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_UNSAVED_CHANGES</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-7</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_NOT_NOW</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-8</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_BAD_PROJECT</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-9</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_CREATE_FAILED</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">-10</p></td> </tr> </tbody> </table> </div> <div class="sect2"> <h3 id="_client_to_server_control">2.6. Client to Server Control</h3> <div class="paragraph"> <p>If the server publishes the <code>:server-control:</code> capability, then clients can also initiate action by the server. For example, a client might implement a 'Save All' option which sends a <code>/nsm/server/save</code> message to the server, rather than requiring the user to switch to the session management interface to effect the save.</p> </div> </div> <div class="sect2"> <h3 id="_server_control_api">2.7. Server Control API</h3> <div class="paragraph"> <p>The session manager not only manages clients via OSC, but it is itself controlled via OSC messages. The server responds to the following messages.</p> </div> <div class="paragraph"> <p>All of the following messages will be responded to, at the sender’s address, with one of the two following messages:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/reply s:path s:message</code></pre> </div> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/error s:path i:error_code s:message</code></pre> </div> </div> <div class="paragraph"> <p>The first parameter of the reply is the path to the message being replied to. The <code>/error</code> reply includes an integer error code (non-zero indicates error). <code>message</code> will be a description of the error.</p> </div> <div class="paragraph"> <p>The possible errors are:</p> </div> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 7. Responses</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Code</th> <th class="tableblock halign-left valign-top">Meaning</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_GENERAL</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">General Error</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_LAUNCH_FAILED</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Launch failed</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_NO_SUCH_FILE</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">No such file</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_NO_SESSION</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">No session is open</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">ERR_UNSAVED_CHANGES</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">Unsaved changes would be lost</p></td> </tr> </tbody> </table> <div class="ulist"> <ul> <li> <p><code>/nsm/server/add s:executable_name</code></p> <div class="ulist"> <ul> <li> <p>Adds a client to the current session.</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/save</code></p> <div class="ulist"> <ul> <li> <p>Saves the current session.</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/open s:project_name</code></p> <div class="ulist"> <ul> <li> <p>Saves the current session and loads a new session.</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/new s:project_name</code></p> <div class="ulist"> <ul> <li> <p>Saves the current session and creates a new session.</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/duplicate s:new_project</code></p> <div class="ulist"> <ul> <li> <p>Saves and closes the current session, makes a copy, and opens it.</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/close</code></p> <div class="ulist"> <ul> <li> <p>Saves and closes the current session.</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/abort</code></p> <div class="ulist"> <ul> <li> <p>Closes the current session WITHOUT SAVING</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/quit</code></p> <div class="ulist"> <ul> <li> <p>Saves and closes the current session and terminates the server.</p> </li> </ul> </div> </li> <li> <p><code>/nsm/server/list</code></p> <div class="ulist"> <ul> <li> <p>Lists available projects. One <code>/reply</code> message will be sent for each existing project.</p> </li> <li> <p>Afer listing the last session one final <code>/reply</code> with <code>/nsm/server/list, ""</code> will be send. That is an empty string.</p> </li> </ul> </div> </li> </ul> </div> <div class="sect3"> <h4 id="_client_to_client_communication">2.7.1. Client to Client Communication</h4> <div class="paragraph"> <p>If the server includes <code>:broadcast:</code> in its capability string, then clients may send broadcast messages to each other through the NSM server. Clients may send messages to the server at the path <code>/nsm/server/broadcast</code>.</p> </div> <div class="paragraph"> <p>The format of this message is as follows:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/server/broadcast s:path [arguments...]</code></pre> </div> </div> <div class="paragraph"> <p>The message will then be relayed to all clients in the session at the path <code>path</code> (with the arguments shifted by one).</p> </div> <div class="paragraph"> <p>For example the message:</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/nsm/server/broadcast /tempomap/update "0,120,4/4:12351234,240,4/4"</code></pre> </div> </div> <div class="paragraph"> <p>Would broadcast the following message to all clients in the session (except for the sender), some of which might respond to the message by updating their own tempo maps.</p> </div> <div class="listingblock"> <div class="content"> <pre class="highlight nowrap"><code class="language-OSC" data-lang="OSC">/tempomap/update "0,120,4/4:12351234,240,4/4"</code></pre> </div> </div> <div class="paragraph"> <p>The Non programs use this feature to establish peer to peer OSC communication by symbolic names (client IDs) without having to remember the OSC URLs of peers across sessions.</p> </div> </div> </div> </div> </div> <div class="sect1"> <h2 id="_api_versions_and_behaviour_changes">3. API Versions and Behaviour Changes</h2> <div class="sectionbody"> <div class="paragraph"> <p>Here we will document all technical changes or differences in behaviour together with their API and project version numbers. The term "original" refers to Non Session Manager and "new" refers to New Session Manager.</p> </div> <div class="paragraph"> <p>Version numbers follow <a href="https://semver.org/spec/v2.0.0.html">Semantic Versioning 2.0.0</a></p> </div> <div class="listingblock"> <div class="title">Semantic Versioning Scheme</div> <div class="content"> <pre class="highlight"><code>Given a version number MAJOR.MINOR.PATCH, increment the: MAJOR version when you make incompatible API changes, MINOR version when you add functionality in a backwards compatible manner, and PATCH version when you make backwards compatible bug fixes.</code></pre> </div> </div> <table class="tableblock frame-all grid-all stripes-even stretch"> <caption class="title">Table 8. NSM Version Numbers</caption> <colgroup> <col style="width: 50%;"> <col style="width: 50%;"> </colgroup> <thead> <tr> <th class="tableblock halign-left valign-top">Subject</th> <th class="tableblock halign-left valign-top">Version</th> </tr> </thead> <tbody> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">Non Session Manager at moment of fork</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">1.2 (June 2020)</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">Non Session Manager API</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">1.0 <a href="https://github.com/original-male/non/blob/master/session-manager/src/nsmd.C">NON nsmd.C</a></p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">Original API Document</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">1.0 <a href="http://non.tuxfamily.org/nsm/API.html">non.tuxfamily.org/nsm/API.html</a></p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">New Session Manager</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">1.6.0</p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">New Session Manager API</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">1.1.2 <a href="https://github.com/jackaudio/new-session-manager/blob/master/src/nsmd.cpp">NEW nsmd.cpp</a></p></td> </tr> <tr> <td class="tableblock halign-left valign-top"><p class="tableblock">New API Document</p></td> <td class="tableblock halign-left valign-top"><p class="tableblock">1.5.0 <a href="#">Here</a></p></td> </tr> </tbody> </table> <div class="sect2"> <h3 id="_guidelines">3.1. Guidelines</h3> <div class="paragraph"> <p>The most important factor in decision making is to keep client compatibility at 100%. No client will ever receive an unrequested OSC message except those in API 1.0.0.</p> </div> <div class="paragraph"> <p>Messages that drastically change existing <code>/nsm/client/</code> or <code>/nsm/server</code> behaviour require an inrecement to <code>API_VERSION_MAJOR</code>, which we want to avoid.</p> </div> <div class="paragraph"> <p><code>nsmd</code> checks if the clients <code>API_VERSION_MAJOR</code> is greater than its own and refuses the client with <code>ERR_INCOMPATIBLE_API</code>.</p> </div> <div class="paragraph"> <p>All changes (that concern client/server behaviour) that increment <code>API_VERSION_MINOR</code> will be request-only or gated by new capabilities (e.g. <code>:optional-gui:</code>). <code>nsmd</code> will not send any messages if a capability was not sent by the client in <a href="#_announce"><code>announce</code></a>. This includes mostly optional features about requesting extra information.</p> </div> <div class="paragraph"> <p>New actions for server-control, for example a hypothetical <code>/nsm/server/save_as</code>, which would be triggered by the client and would only be <strong>answered</strong> by the server ("no unrequested message") will increment <code>API_VERSION_MINOR</code>.</p> </div> <div class="paragraph"> <p>All changes that increment <code>API_VERSION_PATCH</code> will not have any effect on behaviour, except to fix clear problems, where "problem" is defined by having a different effect than described in this document, which includes technical problems such as crashes.</p> </div> <div class="paragraph"> <p>All messages regarding GUI-communication that start with <code>/nsm/gui/…​</code> were undocumented in API 1.0.0 and only used by <code>non-session-manager</code> / <code>nsm-legacy-gui</code>. Until properly documented in this document this part of the API is considered unstable and may change at any time without notice. However, when changing already existing messages and behaviour it MAY increment <code>API_VERSION_MINOR</code> or <code>API_VERSION_PATCH</code>. In that case it will appear in the list below.</p> </div> <div class="paragraph"> <p>Last factor of compatibility is that any unknown message sent to <code>nsmd</code> will just print a warning message to stdout, but will otherwise be ignored. This secures a stable server, even when a client misbehaves and sends too-new messages outside of announced :capabilites:</p> </div> </div> <div class="sect2"> <h3 id="_changes_in_api_version_1_1_0">3.2. Changes in API Version 1.1.0</h3> <div class="paragraph"> <p>Rewritten API document without code changes to adapt to existing code or existing client behaviour:</p> </div> <div class="ulist"> <ul> <li> <p>Changed versioning scheme to Semantic Versioning with three positions Major.Minor.Patch</p> </li> <li> <p><a href="#_quit_or_exit">Quit or Exit</a> SHOULD hide instead of exiting when :optional-gui: is supported and MAY not act on the quit through menu otherwise.</p> </li> <li> <p><a href="#server-to-client-control-messages-open">Open</a>: Make clear that there are only certain possibilities for save paths. We added MUST because the rule was just implied before.</p> </li> <li> <p><a href="#server-to-client-control-messages-open">Open</a>: Make clear that the delimiter for multi-jack clients is "/".</p> </li> <li> <p><a href="#_optional_gui">Optional GUI</a> SHOULD start hidden, always after a fresh add to the session. After that saving the visibility state may override it for next time.</p> </li> <li> <p><a href="#_progress">Progress</a> MUST be announced in :capabilities: . Before there was a lower case "should", which means nothing. Parallel-examples in the specs cleary say that supporting optional features must be announced first.</p> <div class="ulist"> <ul> <li> <p>Same for <a href="#_dirtiness">Dirtiness</a> and <a href="#_status_messsages">Status Messsages</a>.</p> </li> </ul> </div> </li> <li> <p><a href="#_status_messsages">Status Messsages</a> have priority numbers between 0 and 3, so they MUST send that. It was never an arbitrary value.</p> </li> </ul> </div> <div class="paragraph"> <p>Code changes:</p> </div> <div class="ulist"> <ul> <li> <p><a href="#_server_control_api">Server Control API</a>: <code>/nsm/server/list</code> chain of single OSC messages, one for each session, is now finalized with sending and empty string "" as session name. Previously this was just a symbolically irrelevant console message <code>"Done."</code></p> </li> <li> <p>Replies to <code>/nsm/server/save</code> etc. will now be sent back to the sender and not falsely to the last client who replied to <code>/nsm/client/save</code>. This alone would only require API_VERSION_PATCH increment, but we are already incrementing minor.</p> </li> <li> <p><a href="#_server_control_api">Server Control API</a>: <code>/nsm/server/add</code> was replying with an undocumented error code on success. Instead, as this document always specificed, it now sends <code>"/reply", path, "Launched."</code>. Again, this would have been just API_VERSION_PATCH on its own.</p> </li> </ul> </div> <div class="paragraph"> <p>Undocumented (Unstable) <code>/nsm/gui</code> protocol</p> </div> <div class="ulist"> <ul> <li> <p>Send client status after a GUI attaches to running server. This was not happening before, but it was the intention. It was just broken in nsmd.cpp. This alone would only require API_VERSION_PATCH increment, but we are already incrementing minor.</p> </li> <li> <p>Send label "launch error!" when a program is added (or loaded) that does not exist in $PATH. This requires no adaptation of any client, server or GUI because labels are arbitrary already and this is not meant for automatic parsing, but as user information.</p> </li> <li> <p><code>/nsm/gui/session/name</code> will now always send the same parameter format, regardless of how the session was opened: simple-session-name, relative session path with subdirs below session-root.</p> </li> <li> <p>When a GUI announces itself to nsmd it will receive the absolute path to the session directory through the message <code>/nsm/gui/session/root</code>. This is not a new addition but was already in non-session-manager git.</p> </li> </ul> </div> </div> <div class="sect2"> <h3 id="_changes_in_api_version_1_1_1">3.3. Changes in API Version 1.1.1</h3> <div class="ulist"> <ul> <li> <p>Server-capability :optional-gui: is now mandatory for SERVER implementations. Reasoning: This is an important core feature of NSM and thus will be treated as such by guaranteeing it to exist. After looking at all currently known clients and server-implementations it turns out that all servers support :optional-gui: and the vast majority of clients not only support it, but actually assume it and do <em>not</em> test for the server capability, as it was written in this document. There are now two choices: Adjust this document to the (good) reality or consider all clients broken. Summary: We consider this API document wrong and therefore fix it, thus increasing API version patch-level from 1.1.0 to 1.1.1</p> </li> <li> <p>Add API-section "Subdirectories / Hierarchical Structure" that explains the session directory. This behaviour was already the case for nsm-legacy-gui and nsmd 1.5.0 was patched to adhere to this behaviour more strictly as well, removing false session entries in 3rd party clients such as Agordejo.</p> </li> </ul> </div> </div> <div class="sect2"> <h3 id="_changes_in_api_version_1_1_2">3.4. Changes in API Version 1.1.2</h3> <div class="ulist"> <ul> <li> <p>nsmd now follows the XDG Base Directory Specifications for it’s session root and lock files. This if of no consequence to clients but required documentation nevertheless, which was described as "background information" in the chapters for lock files and daemon disovery.</p> </li> <li> <p>nsmd now gracefully handles read-only <code>session.nsm</code> files. This theoretically enables read-only sessions and session-templates. It is included in the patch-level because this was marked as a long-standing <code>FIXME</code> in the code by the original author. Or in other words: just a bug fix.</p> </li> </ul> </div> </div> </div> </div> </div> <div id="footer"> <div id="footer-text"> Version API 1.1.2<br> Last updated 2022-04-15 00:54:20 +0200 </div> </div> </body> </html>07070100000009000081A40000000000000000000000016258A65C000083DC000000000000000000000000000000000000003F00000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Asciidoctor 2.0.17"> <meta name="author" content="Nils Hilbricht"> <title>New Session Manager Documentation</title> <style> /*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */ /* Uncomment the following line when using as a custom stylesheet */ /* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */ html{font-family:sans-serif;-webkit-text-size-adjust:100%} a{background:none} a:focus{outline:thin dotted} a:active,a:hover{outline:0} h1{font-size:2em;margin:.67em 0} b,strong{font-weight:bold} abbr{font-size:.9em} abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none} dfn{font-style:italic} hr{height:0} mark{background:#ff0;color:#000} code,kbd,pre,samp{font-family:monospace;font-size:1em} pre{white-space:pre-wrap} q{quotes:"\201C" "\201D" "\2018" "\2019"} small{font-size:80%} sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} sup{top:-.5em} sub{bottom:-.25em} img{border:0} svg:not(:root){overflow:hidden} figure{margin:0} audio,video{display:inline-block} audio:not([controls]){display:none;height:0} fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} legend{border:0;padding:0} button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} button,input{line-height:normal} button,select{text-transform:none} button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer} button[disabled],html input[disabled]{cursor:default} input[type=checkbox],input[type=radio]{padding:0} button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} textarea{overflow:auto;vertical-align:top} table{border-collapse:collapse;border-spacing:0} *,::before,::after{box-sizing:border-box} html,body{font-size:100%} body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased} a:hover{cursor:pointer} img,object,embed{max-width:100%;height:auto} object,embed{height:100%} img{-ms-interpolation-mode:bicubic} .left{float:left!important} .right{float:right!important} .text-left{text-align:left!important} .text-right{text-align:right!important} .text-center{text-align:center!important} .text-justify{text-align:justify!important} .hide{display:none} img,object,svg{display:inline-block;vertical-align:middle} textarea{height:auto;min-height:50px} select{width:100%} .subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0} a{color:#2156a5;text-decoration:underline;line-height:inherit} a:hover,a:focus{color:#1d4b8f} a img{border:0} p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} p aside{font-size:.875em;line-height:1.35;font-style:italic} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} h1{font-size:2.125em} h2{font-size:1.6875em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} h4,h5{font-size:1.125em} h6{font-size:1em} hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em} em,i{font-style:italic;line-height:inherit} strong,b{font-weight:bold;line-height:inherit} small{font-size:60%;line-height:inherit} code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} @media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} h1{font-size:2.75em} h2{font-size:2.3125em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} h4{font-size:1.4375em}} table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal} table thead,table tfoot{background:#f7f8f7} table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} table tr.even,table tr.alt{background:#f8f8f7} table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} .center{margin-left:auto;margin-right:auto} .stretch{width:100%} .clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table} .clearfix::after,.float-group::after{clear:both} :not(pre).nobreak{word-wrap:normal} :not(pre).nowrap{white-space:nowrap} :not(pre).pre-wrap{white-space:pre-wrap} :not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed} pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit} pre>code{display:block} pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal} em em{font-style:normal} strong strong{font-weight:400} .keyseq{color:rgba(51,51,51,.8)} kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} .keyseq kbd:first-child{margin-left:0} .keyseq kbd:last-child{margin-right:0} .menuseq,.menuref{color:#000} .menuseq b:not(.caret),.menuref{font-weight:inherit} .menuseq{word-spacing:-.02em} .menuseq b.caret{font-size:1.25em;line-height:.8} .menuseq i.caret{font-weight:bold;text-align:center;width:.45em} b.button::before,b.button::after{position:relative;top:-1px;font-weight:400} b.button::before{content:"[";padding:0 3px 0 2px} b.button::after{content:"]";padding:0 2px 0 3px} p a>code:hover{color:rgba(0,0,0,.9)} #header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} #header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table} #header::after,#content::after,#footnotes::after,#footer::after{clear:both} #content{margin-top:1.25em} #content::before{content:none} #header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} #header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf} #header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px} #header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap} #header .details span:first-child{margin-left:-.125em} #header .details span.email a{color:rgba(0,0,0,.85)} #header .details br{display:none} #header .details br+span::before{content:"\00a0\2013\00a0"} #header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} #header .details br+span#revremark::before{content:"\00a0|\00a0"} #header #revnumber{text-transform:capitalize} #header #revnumber::after{content:"\00a0"} #content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} #toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em} #toc>ul{margin-left:.125em} #toc ul.sectlevel0>li>a{font-style:italic} #toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} #toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} #toc li{line-height:1.3334;margin-top:.3334em} #toc a{text-decoration:none} #toc a:active{text-decoration:underline} #toctitle{color:#7a2518;font-size:1.2em} @media screen and (min-width:768px){#toctitle{font-size:1.375em} body.toc2{padding-left:15em;padding-right:0} #toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} #toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} #toc.toc2>ul{font-size:.9em;margin-bottom:0} #toc.toc2 ul ul{margin-left:0;padding-left:1em} #toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} body.toc2.toc-right{padding-left:0;padding-right:15em} body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}} @media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} #toc.toc2{width:20em} #toc.toc2 #toctitle{font-size:1.375em} #toc.toc2>ul{font-size:.95em} #toc.toc2 ul ul{padding-left:1.25em} body.toc2.toc-right{padding-left:0;padding-right:20em}} #content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px} #content #toc>:first-child{margin-top:0} #content #toc>:last-child{margin-bottom:0} #footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em} #footer-text{color:hsla(0,0%,100%,.8);line-height:1.44} #content{margin-bottom:.625em} .sect1{padding-bottom:.625em} @media screen and (min-width:768px){#content{margin-bottom:1.25em} .sect1{padding-bottom:1.25em}} .sect1:last-child{padding-bottom:0} .sect1+.sect1{border-top:1px solid #e7e7e9} #content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} #content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} #content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} #content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} #content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} details{margin-left:1.25rem} details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent} details>summary::-webkit-details-marker{display:none} details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)} details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)} details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem} .admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} table.tableblock.fit-content>caption.title{white-space:nowrap;width:0} .paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)} .admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} .admonitionblock>table td.icon{text-align:center;width:80px} .admonitionblock>table td.icon img{max-width:none} .admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} .exampleblock>.content>:first-child{margin-top:0} .exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} .sidebarblock>:first-child{margin-top:0} .sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} .exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} .literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8} .literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)} .listingblock>.content{position:relative} .listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5} .listingblock:hover code[data-lang]::before{display:block} .listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5} .listingblock.terminal pre .command:not([data-prompt])::before{content:"$"} .listingblock pre.highlightjs{padding:0} .listingblock pre.highlightjs>code{padding:1em;border-radius:4px} .listingblock pre.prettyprint{border-width:0} .prettyprint{background:#f7f7f8} pre.prettyprint .linenums{line-height:1.45;margin-left:2em} pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0} pre.prettyprint li code[data-lang]::before{opacity:1} pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none} table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none} table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal} table.linenotable td.code{padding-left:.75em} table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} pre.pygments span.linenos{display:inline-block;margin-right:.75em} .quoteblock{margin:0 1em 1.25em 1.5em;display:table} .quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em} .quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} .quoteblock blockquote{margin:0;padding:0;border:0} .quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} .quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} .quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right} .verseblock{margin:0 1em 1.25em} .verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} .verseblock pre strong{font-weight:400} .verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} .quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} .quoteblock .attribution br,.verseblock .attribution br{display:none} .quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} .quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none} .quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0} .quoteblock.abstract{margin:0 1em 1.25em;display:block} .quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center} .quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf} .quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0} .quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem} .quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0} p.tableblock:last-child{margin-bottom:0} td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere} td.tableblock>.content>:last-child{margin-bottom:-1.25em} table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} table.grid-all>*>tr>*{border-width:1px} table.grid-cols>*>tr>*{border-width:0 1px} table.grid-rows>*>tr>*{border-width:1px 0} table.frame-all{border-width:1px} table.frame-ends{border-width:1px 0} table.frame-sides{border-width:0 1px} table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0} table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0} table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0} table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0} table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7} th.halign-left,td.halign-left{text-align:left} th.halign-right,td.halign-right{text-align:right} th.halign-center,td.halign-center{text-align:center} th.valign-top,td.valign-top{vertical-align:top} th.valign-bottom,td.valign-bottom{vertical-align:bottom} th.valign-middle,td.valign-middle{vertical-align:middle} table thead th,table tfoot th{font-weight:bold} tbody tr th{background:#f7f8f7} tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} p.tableblock>code:only-child{background:none;padding:0} p.tableblock{font-size:1em} ol{margin-left:1.75em} ul li ol{margin-left:1.5em} dl dd{margin-left:1.125em} dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none} ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em} ul.unstyled,ol.unstyled{margin-left:0} li>p:empty:only-child::before{content:"";display:inline-block} ul.checklist>li>p:first-child{margin-left:-1em} ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em} ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em} ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em} ul.inline>li{margin-left:1.25em} .unstyled dl dt{font-weight:400;font-style:normal} ol.arabic{list-style-type:decimal} ol.decimal{list-style-type:decimal-leading-zero} ol.loweralpha{list-style-type:lower-alpha} ol.upperalpha{list-style-type:upper-alpha} ol.lowerroman{list-style-type:lower-roman} ol.upperroman{list-style-type:upper-roman} ol.lowergreek{list-style-type:lower-greek} .hdlist>table,.colist>table{border:0;background:none} .hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} td.hdlist1{font-weight:bold;padding-bottom:1.25em} td.hdlist2{word-wrap:anywhere} .literalblock+.colist,.listingblock+.colist{margin-top:-.5em} .colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top} .colist td:not([class]):first-child img{max-width:none} .colist td:not([class]):last-child{padding:.25em 0} .thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd} .imageblock.left{margin:.25em .625em 1.25em 0} .imageblock.right{margin:.25em 0 1.25em .625em} .imageblock>.title{margin-bottom:0} .imageblock.thumb,.imageblock.th{border-width:6px} .imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} .image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} .image.left{margin-right:.625em} .image.right{margin-left:.625em} a.image{text-decoration:none;display:inline-block} a.image object{pointer-events:none} sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} sup.footnote a,sup.footnoteref a{text-decoration:none} sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline} #footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} #footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0} #footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em} #footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em} #footnotes .footnote:last-of-type{margin-bottom:0} #content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} div.unbreakable{page-break-inside:avoid} .big{font-size:larger} .small{font-size:smaller} .underline{text-decoration:underline} .overline{text-decoration:overline} .line-through{text-decoration:line-through} .aqua{color:#00bfbf} .aqua-background{background:#00fafa} .black{color:#000} .black-background{background:#000} .blue{color:#0000bf} .blue-background{background:#0000fa} .fuchsia{color:#bf00bf} .fuchsia-background{background:#fa00fa} .gray{color:#606060} .gray-background{background:#7d7d7d} .green{color:#006000} .green-background{background:#007d00} .lime{color:#00bf00} .lime-background{background:#00fa00} .maroon{color:#600000} .maroon-background{background:#7d0000} .navy{color:#000060} .navy-background{background:#00007d} .olive{color:#606000} .olive-background{background:#7d7d00} .purple{color:#600060} .purple-background{background:#7d007d} .red{color:#bf0000} .red-background{background:#fa0000} .silver{color:#909090} .silver-background{background:#bcbcbc} .teal{color:#006060} .teal-background{background:#007d7d} .white{color:#bfbfbf} .white-background{background:#fafafa} .yellow{color:#bfbf00} .yellow-background{background:#fafa00} span.icon>.fa{cursor:default} a span.icon>.fa{cursor:inherit} .admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} .admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c} .admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} .admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900} .admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400} .admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000} .conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} .conum[data-value] *{color:#fff!important} .conum[data-value]+b{display:none} .conum[data-value]::after{content:attr(data-value)} pre .conum[data-value]{position:relative;top:-.125em} b.conum *{color:inherit!important} .conum:not([data-value]):empty{display:none} dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} .print-only{display:none!important} @page{margin:1.25cm .75cm} @media print{*{box-shadow:none!important;text-shadow:none!important} html{font-size:80%} a{color:inherit!important;text-decoration:underline!important} a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} abbr[title]{border-bottom:1px dotted} abbr[title]::after{content:" (" attr(title) ")"} pre,blockquote,tr,img,object,svg{page-break-inside:avoid} thead{display:table-header-group} svg{max-width:100%} p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} #header,#content,#footnotes,#footer{max-width:none} #toc,.sidebarblock,.exampleblock>.content{background:none!important} #toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important} body.book #header{text-align:center} body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em} body.book #header .details{border:0!important;display:block;padding:0!important} body.book #header .details span:first-child{margin-left:0!important} body.book #header .details br{display:block} body.book #header .details br+span::before{content:none!important} body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} .listingblock code[data-lang]::before{display:block} #footer{padding:0 .9375em} .hide-on-print{display:none!important} .print-only{display:block!important} .hide-for-print{display:none!important} .show-for-print{display:inherit!important}} @media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem} .sect1{padding:0!important} .sect1+.sect1{border:0} #footer{background:none} #footer-text{color:rgba(0,0,0,.6);font-size:.9em}} @media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}} </style> </head> <body class="article"> <div id="header"> <h1>New Session Manager Documentation</h1> <div class="details"> <span id="author" class="author">Nils Hilbricht</span><br> <span id="revnumber">version 1.6.0</span> </div> </div> <div id="content"> <div id="preamble"> <div class="sectionbody"> <div class="ulist"> <ul> <li> <p><a href="https://github.com/jackaudio/new-session-manager">Sourcecode</a></p> </li> <li> <p><a href="https://github.com/jackaudio/new-session-manager/issues">Bug and Issue Tracker</a></p> </li> <li> <p><a href="api/index.html">API</a> document that describes all OSC Messages</p> </li> <li> <p><a href="http://non.tuxfamily.org/session-manager/doc/MANUAL.html">Legacy-GUI Manual</a>. The original Non-Session-Manager GUI manual is still valid.</p> </li> </ul> </div> </div> </div> <div class="sect1"> <h2 id="_introduction">Introduction</h2> <div class="sectionbody"> <div class="paragraph"> <p>New Session Manager (NSM) is a tool to assist music production by grouping standalone programs into sessions. Your workflow becomes easy to manage, robust and fast by leveraging the full potential of cooperative applications.</p> </div> <div class="paragraph"> <p>NSM continues to be free in every sense of the word: free of cost, free to share and use, free of spyware or ads, free-and-open-source.</p> </div> <div class="paragraph"> <p>You can create a session, or project, add programs to it and then use commands to save, start/stop, hide/show all programs at once, or individually. At a later date you can then re-open the session and continue where you left off.</p> </div> <div class="paragraph"> <p>All files belonging to the session will be saved in the same directory.</p> </div> <div class="paragraph"> <p>If you are a user (and not a programmer or packager) everything you need is to install NSM through your distributions package manager and, highly recommended, Agordejo as a GUI (see below).</p> </div> <div class="paragraph"> <p>To learn NSM you don’t need to know the background information from our documentation, which is aimed at developers that want to implement NSM support in their programs. Learn the GUI, not the server and protocol.</p> </div> </div> </div> <div class="sect1"> <h2 id="_bullet_points">Bullet Points</h2> <div class="sectionbody"> <div class="ulist"> <ul> <li> <p>Drop-In replacement for the non-session-manager daemon nsmd and tools (e.g. jackpatch)</p> </li> <li> <p>Simple and hassle-free build system to make packaging easy</p> </li> <li> <p>Possibility to react to sensible bug fixes that would not have been integrated original nsmd</p> </li> <li> <p>Stay upwards and downwards compatible with original nsmd</p> </li> <li> <p>Conservative and hesitant in regards to new features and behaviour-changes, but possible in principle</p> </li> <li> <p>Keep the session-manager separate from the other NON* tools Mixer, Sequencer and Timeline.</p> </li> <li> <p>Protect nsmd from vanishing from the internet one day.</p> </li> <li> <p>The goal is to become the de-facto standard music session manager for Linux distributions</p> </li> </ul> </div> </div> </div> <div class="sect1"> <h2 id="_user_interface">User Interface</h2> <div class="sectionbody"> <div class="paragraph"> <p>It is highly recommended to use Agordejo ( <a href="https://www.laborejo.org/agordejo/" class="bare">https://www.laborejo.org/agordejo/</a> ) as graphical user interface. In fact, if you install Agordejo in your distribution it will install NSM as dependency and you don’t need to do anything yourself with this software package.</p> </div> <div class="paragraph"> <p>This repository also contains the legacy FLTK interface simply called <code>nsm-legacy-gui</code>, symlinked to <code>non-session-manager</code> for backwards compatibility. (e.g. autostart scripts etc.)</p> </div> </div> </div> <div class="sect1"> <h2 id="_supported_clients">Supported Clients</h2> <div class="sectionbody"> <div class="paragraph"> <p>While NSM can start and stop any program it only becomes convenient if clients specifically implement support. This enables saving and hiding the GUI, amongst other features. Documentation and tutorials for software-developers will be added at a later date.</p> </div> </div> </div> </div> <div id="footer"> <div id="footer-text"> Version 1.6.0<br> Last updated 2022-04-15 00:54:20 +0200 </div> </div> </body> </html>0707010000000A000041ED0000000000000000000000036258A65C00000000000000000000000000000000000000000000003800000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src0707010000000B000081A40000000000000000000000016258A65C00004EEE000000000000000000000000000000000000004000000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/LICENSEfrom: https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt Attribution-ShareAlike 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-ShareAlike 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. Additional offer from the Licensor -- Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. c. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. 0707010000000C000041ED0000000000000000000000026258A65C00000000000000000000000000000000000000000000003C00000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/api0707010000000D000081A40000000000000000000000016258A65C00003765000000000000000000000000000000000000004400000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/api/LICENSECreative Commons Creative Commons Legal Code Attribution-ShareAlike 2.5 https://creativecommons.org/licenses/by-sa/2.5/legalcode CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. License THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 1. Definitions "Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with a number of other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License. "Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License. "Licensor" means the individual or entity that offers the Work under the terms of this License. "Original Author" means the individual or entity who created the Work. "Work" means the copyrightable work of authorship offered under the terms of this License. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike. 2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws. 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works; to create and reproduce Derivative Works; to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works; to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works. For the avoidance of doubt, where the work is a musical composition: Performance Royalties Under Blanket Licenses. Licensor waives the exclusive right to collect, whether individually or via a performance rights society (e.g. ASCAP, BMI, SESAC), royalties for the public performance or public digital performance (e.g. webcast) of the Work. Mechanical Rights and Statutory Royalties. Licensor waives the exclusive right to collect, whether individually or via a music rights society or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions). Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor waives the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions). The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved. 4. Restrictions.The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any credit as required by clause 4(c), as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any credit as required by clause 4(c), as requested. You may distribute, publicly display, publicly perform, or publicly digitally perform a Derivative Work only under the terms of this License, a later version of this License with the same License Elements as this License, or a Creative Commons iCommons license that contains the same License Elements as this License (e.g. Attribution-ShareAlike 2.5 Japan). You must include a copy of, or the Uniform Resource Identifier for, this License or other license specified in the previous sentence with every copy or phonorecord of each Derivative Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Derivative Works that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder, and You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Derivative Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Derivative Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Derivative Work itself to be made subject to the terms of this License. If you distribute, publicly display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or (ii) if the Original Author and/or Licensor designate another party or parties (e.g. a sponsor institute, publishing entity, journal) for attribution in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). Such credit may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear where any other comparable authorship credit appears and in a manner at least as prominent as such other comparable authorship credit. 5. Representations, Warranties and Disclaimer UNLESS OTHERWISE AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE MATERIALS, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. Termination This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works or Collective Works from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 8. Miscellaneous Each time You distribute or publicly digitally perform the Work or a Collective Work, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, neither party will use the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. Creative Commons may be contacted at https://creativecommons.org/. 0707010000000E000081A40000000000000000000000016258A65C000090F2000000000000000000000000000000000000004700000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/api/index.adoc//// This is "asciidoctor", not plain "asciidoc". https://asciidoctor.org/docs/user-manual/ 100 characters per line (soft limit). //// //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 2.5 International License. To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/2.5/legalcode or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/API/LICENSE. //// //// The revnumber API 1.1.1 below is autogenerated. Please do not touch this line. //// :authors: Jonathan Moore Liles, Nils Hilbricht :revnumber: API 1.1.2 :revremark: License CC-By-SA v2.5 :iconfont-remote!: :!webfonts: :sectnums: :sectnumlevels: 4 :toc: :toc-title: Table of Contents :toclevels: 4 = New Session Manager - API IMPORTANT: "New Session Manager" is a community version of the link:http://non.tuxfamily.org/nsm/API.html["Non Session Manager" by Jonathan Moore Liles], who also wrote the majority of this API document, especially the API itself. *The API is the same*. Any technical changes or differences in behaviour are described in <<API Versions and Behaviour Changes>>. All other changes to this document can be reviewed by accessing the git log. This document is licensed under CC-By-Sa v2.5. See link:https://github.com/jackaudio/new-session-manager/tree/master/docs/src/api[LICENSE] IMPORTANT: The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in link:https://tools.ietf.org/html/rfc2119[RFC 2119]. The "New Session Manager"-API is used by many music and audio programs in Linux distributions to allow any number of independent programs to be managed together as part of a logical session (i.e. a song). Thus, operations such as loading and saving are synchronized. The API comprises a simple Open Sound Control (OSC) based protocol, along with some behavioral guidelines, which can easily be implemented by various applications. This project contains a program called `nsmd` which is an implementation of the server side of the NSM API. `nsmd` can be controlled by direct OSC messages, or (more commonly) a GUI: Included in this package is the `nsm-legacy-gui`, which gets symlinked to "non-session-manager`. Another GUI is "Agordejo". Other applications exist that (partially) support the NSM API and are able to load clients, but they do not use the New-Session-Manager (or Non-Session-Manager) implementation and are therefore out of scope for this document. However, the same server-side API can also be implemented by other programs (such as Carla), although consistency and robustness will likely suffer if non-NSM compliant clients are allowed to participate in a session. There is no direct dependency for client implementations, as long as they can send and receive OSC. Some clients use `liblo` (the OSC library), which becomes a dependency if you choose to implement NSM-support with the provided header file `nsm.h` (`extras/nsm.h/nsm.h` in the git repository). Some clients use the provided single-file python library `pynsm` (`extras/pynsm/nsmclient.py` in the git repository) which has no dependencies outside the Python3 standard library. The aim of this project is to thoroughly define the behavior required of clients. Often the difficulty with other session-management approaches has been not in implementing code-support for them, but in not defining rules and behaviour clearly enough. As written above unambiguous rules are created by using RFC 2119 in this document. For the good of the user, these rules are meant to be followed and are non-negotiable. If an application does not conform to this specification it should be considered broken. Consistency across applications under session management is very important for a good user experience. == Client Behavior Under Session Management Most graphical applications make available to the user a common set of file operations, typically presented under a File or Project menu. These are: New, Open, Save, Save As, Close and Quit or Exit. The following sub-sections describe how these options should behave when the application is part of an NSM session. These rules only apply when session management is active, that is, after the `announce` handshake described in the <<NSM OSC Protocol>> section. In order to provide a consistent and predictable user experience, it is critically important for applications to adhere to these guidelines. === File Menu ==== New This option MAY empty/reset the current file or project (possibly after user confirmation). It MUST NOT allow the user to create a new project/file in another location. ==== Open This option MUST be disabled. The application MAY elect to implement an option called "Import into Session", which creates a copy of a file/project which is then saved at the session path provided by NSM. ==== Save This option should behave as normal, saving the current file/project as established by the NSM `open` message. This option MUST NOT present the user with a choice of where to save the file. ==== Save As This option MUST be disabled. The application MAY elect to implement an option called 'Export from Session', which creates a copy of the current file/project which is then saved in a user-specified location outside of the session path provided by NSM. ==== Close (as distinguished from Quit or Exit) This option MUST be disabled unless its meaning is to disconnect the application from session management. ==== Quit or Exit This option MAY behave as normal (possibly asking the user to confirm exiting), or MAY do nothing to only allow quit from the session-manager control. When the client supports :optional-gui: this option SHOULD be replaced with hiding the client's GUI so a quit by window manager hides. === Data Storage ==== Internal Files All project specific data created by a client MUST be stored in the per-client storage area provided by NSM. This includes all recorded audio and MIDI files, snapshots, etc. Only global configuration items, exports, and renders of the project may be stored elsewhere (wherever the user specifies). ==== External Files Files required by the project but external to it (typically read-only data such as audio samples) SHOULD be referenced by creating a symbolic link within the assigned session area, and then referring to the symlink. This allows sessions to be archived and transported simply (e.g. with "tar -h") by tools that have no knowledge of the project formats of the various clients in the session. The symlinks thus created should, at the very least, be named after the files they refer to. Some unique component may be required to prevent collisions. ==== Session Root and Session Directories Client programs MUST NOT handle the following themselves. This section is background-information. NSM follows the link:https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html[XDG Base Directory Specifications] All existing and new sessions are directories below the session-root, which defaults to `$XDG_DATA_HOME/nsm/`, which usually results in `$HOME/.local/share/nsm/`. Each session directory contains a file `session.nsm` with one client per line `name:executable:UID\n` For example: ``` JACKPatch:jackpatch:nBEIQ jack_mixer:jack_mixer:nTXHV Carla-Rack:carla-rack:nFAOD ``` `nsmd` loads and saves this file, client names are their self-reported names. The file format is final and frozen. Additions or changes SHALL NOT be made. ===== Subdirectories / Hierarchical Structure Subdirectories MAY be made to organize sessions into meaningful structures, such as album/track or composer/genre/piece. For example: `Johann Sebastian Bach/Kantaten/Wie schön leuchtet der Morgenstern`. Which results in the same directory structure on disk. Session names can contain any characters that are supported by the underlying file system, usually UTF-8. Subdirectories are created by either `nsmd` itself or by the users themselves, through their file manager or a GUI (while the session is not open). The project_name from `/nsm/server/new s:project_name` accepts the format `a/b/c/d`. Any session itself MUST be a "leaf" in this directory tree. A session MUST NOT contain further session subdirectories: any directory that contains a file `session.nsm` is the final element in the hierarchy. ===== Write-Protection for Session Templates Write protection for a whole session directory can either happen by "accident" (files from another user, a network mount etc.) or on purpose, to protect a session template against accidental changes. The latter is possible with a recursive `chown`, `chmod` or `chattr -R +i session-dir`. nsmd itself just checks if `session.nsm` is read-only. In this case it will not send the save command to it's session clients. This does not prevent hypothetical problems when the user triggers a clients internal save command in a write protected directory. Clients SHOULD handle their write protected save files themselves. Advanced contraptions, like overlay filesystems or copy-on-write hardlinks to create read-only sessions without the clients noticing, are out of scope for nsm. ===== Lockfiles Because multiple `nsmd` can run at the same time we need to prevent accidental write-access to the same session by different nsm-daemons, and subsequently GUIs. Therefore each currently open session creates a lockfile under `$XDG_RUNTIME_DIR/nsm/` (usually `/run/user/XXXX/nsm/`) that tells `nsmd` to not open such a locked session. This directory gets cleaned by the operating system, preventing sessions to stay locked after e.g. a power failure. The lockfile is named after the simple session name combined with a numeric ID for the session root. It is possible that two `nsmd` opened two different session roots, both with the same simple session name, e.g. "my song". Lockfiles are able to distinguish between those and will not prevent access in this scenario. The numeric ID is a djb2 hash modulo (%) 65521 of the session root directory (see `src/file.cpp` function `simple_hash()`). The lockfile contains, on separate lines: * The absolute path to the session, including the root-dir, which could be overriden by `nsmd --session-root`, allowing two sessions of the same basic name in different roots. * the OSC URL of the server that runs this session, the same as `$NSM_URL`. * the PID of `nsmd` Example: ``` /home/johann/.local/share/nsm/cantatas/easter1751 osc.udp://myuser.localdomain:11287/ 3022 ``` ===== Daemon Discovery Each running `nsmd`, per user, creates a state file under `$XDG_RUNTIME_DIR/nsm/d/` (usually `/run/user/XXXX/nsm/d/`) that can be used to look up running daemons, even if no session is loaded. The name of the file is `nsmd` PID and the files contain their daemons osc.udp URL that is compatible with the --nsm-url parameter of the GUI. This enables you to e.g. start nsmd at boot with a random free port. Server-control programs such as GUIs can then use this to look for running servers without requiring the user to look up and input an osc URL manually as command line parameter. == NSM OSC Protocol All message parameters are REQUIRED. All messages MUST be sent from the same socket as the `announce` message, using the `lo_send_from` method of liblo or its equivalent, as the server uses the return addresses to distinguish between clients. Clients MUST create thier OSC servers using the same protocol (UDP,TCP) as found in `NSM_URL`. `nsmd` itself is using UDP only. === Establishing a Connection ==== Announce At launch, the client MUST check the environment for the value of `NSM_URL`. If present, the client MUST send the following message to the provided address as soon as it is ready to respond to the `/nsm/client/open` event: [source%nowrap,OSC] ---- /nsm/server/announce s:application_name s:capabilities s:executable_name i:api_version_major i:api_version_minor i:pid ---- If `NSM_URL` is undefined, invalid, or unreachable, then the client should proceed assuming that session management is unavailable. `api_version_major` and `api_version_minor` must be the two parts of the version number of the NSM API as defined by this document. Note that if the application intends to register JACK clients, `application_name` MUST be the same as the name that would normally be passed to `jack_client_open`. For example, Non-Mixer sends "Non-Mixer" as its `application_name`. Applications MUST NOT register their JACK clients until receiving an `open` message; the `open` message will provide a unique client name prefix suitable for passing to JACK. This is probably the most complex requirement of the NSM API, but it isn't difficult to implement, especially if the application simply wishes to delay its initialization process briefly while awaiting the `announce` reply and subsequent `open` message. `capabilities` MUST be a string containing a colon separated list of the special capabilities the client possesses. e.g. `:dirty:switch:progress:` `executable_name` MUST be the executable name that the program was launched with. For C programs, this is simply the value of `argv[0]`. Note that hardcoding the name of the program here is not the same as using, as the user may have launched the program from a script with a different name using exec, or have created a symlink to the program. Getting the correct value in scripting languages like Python can be more challenging. .Available Client Capabilities [options="header", stripes=even] |=== |Name | Description |switch | client is capable of responding to multiple `open` messages without restarting |dirty | client knows when it has unsaved changes |progress | client can send progress updates during time-consuming operations |message | client can send textual status updates |optional-gui | client has an optional GUI |=== ==== Response The server will respond to the client's announce message with the following message: [source%nowrap,OSC] ---- /reply "/nsm/server/announce" s:message s:name_of_session_manager s:capabilities ---- `message` is a welcome message. The value of `name_of_session_manager` will depend on the implementation of the NSM server. It might say "New Session Manager", or it might say "Non Session Manager" etc. This is for display to the user. `capabilities` will be a string containing a colon separated list of special server capabilities. Presently, the server `capabilities` are: .Available Server Capabilities [options="header", stripes=even] |=== |Name | Description |server-control | client-to-server control |broadcast | server responds to /nsm/server/broadcast message |optional-gui | server responds to optional-gui messages. This capability is always present and MUST be supported by any server implementation. |=== A client should not consider itself to be under session management until it receives this response. For example, the Non applications activate their "SM" blinkers at this time. If there is an error, a reply of the following form will be sent to the client: [source%nowrap,OSC] ---- /error "/nsm/server/announce" i:error_code s:error_message ---- The following table defines possible values of `error_code`: .Response codes [options="header", stripes=even] |=== |Code | Meaning |ERR_GENERAL | General Error |ERR_INCOMPATIBLE_API | Incompatible API version |ERR_BLACKLISTED | Client has been blacklisted. |=== === Server to Client Control Messages Compliant clients MUST accept the client control messages described in this section. All client control messages REQUIRE a response. Responses MUST be delivered back to the sender (`nsmd`) from the same socket used by the client in its `announce` message (by using `lo_send_from`) AFTER the action has been completed or if an error is encountered. The required response is described in the subsection for each message. If there is an error and the action cannot be completed, then `error_code` MUST be set to a valid error code (see <<Error Code Definitions>>) and `message` to a string describing the problem (suitable for display to the user). The reply can take one of the following two forms, where path MUST be the `path` of the message being replied to (e.g. "nsm/client/save": [source%nowrap,OSC] ---- /reply s:path s:message ---- [source%nowrap,OSC] ---- /error s:path i:error_code s:message ---- ==== Quit There is no message for this. Clients will receive the Unix SIGTERM signal and MUST close cleanly IMMEDIATELY, without displaying any kind of dialog to the user and regardless of whether or not unsaved changes would be lost. When a session is closed the application will receive this signal soon after having responded to a `save` message. [#server-to-client-control-messages-open] ==== Open [source%nowrap,OSC] ---- /nsm/client/open s:path_to_instance_specific_project s:display_name s:client_id ---- `path_to_instance_specific_project` is a path name in the form client_name.ID, assigned to the client for storing its project data. The client MUST choose one of the four strategies below to save, so that every file in the session can be traced back to a client and, vice versa, a client name.ID can be used to look up all its files. (For example to clean up the session dir) * The client has no state and does not save at all ** and it MUST NOT misuse e.g. ~/.config to save session specific information e.g. synth-instrument settings * The client may use the path client_name.ID directly, resulting in a file client_name.ID in the session directory * The client may append its native file extension (e.g. `.json`) to the path client_name.ID * The client may use the path as directory, creating arbitrary files below, for example recorded .wav. ** and it MUST NOT use the client ID below this point. This way the data stays transferable by hand to another client instance (in another session). ** best case practice is to always use the same file names, for example `client_name.ID/savefile.json` If a project exists at the path, the client MUST immediately open it. If a project does not exist at the path, then the client MUST immediately create and open a new one at the specified path or, for clients which hold all their state in memory, store the path for later use when responding to the `save` message. No file or directory will be created at the specified path by the server. It is up to the client to create what it needs. For clients which HAVE NOT specified the `:switch:` capability, the `open` message will only be delivered once, immediately following the `announce` response. For clients which HAVE specified the `:switch:` capability, the client MUST immediately switch to the specified project or create a new one if it doesn't exist. Clients which are incapable of switching projects or are prone to crashing upon switching MUST NOT include `:switch:` in their capability string. If the user the is allowed to run two or more instances of the application simultaneously then such an application MUST PRE-PEND the provided `client_id` string, followed by "/", to any names it registers with common subsystems (e.g. JACK client names). This ensures that multiple instances of the same application can be restored in any order without scrambling the JACK connections or causing other conflicts. The provided `client_id` will be a concatenation of the value of `application_name` sent by the client in its `announce` message and a unique identifier. Therefore, applications which create single JACK clients can use the value of `client_id` directly as their JACK client name. Applications which register multiple JACK clients (e.g. Carla or Non-Mixer) MUST PRE-PEND `client_id` value, followed by "/", to the client names they register with JACK and the application determined part MUST be unique for that (JACK) client. For example, Carla is a plugin-host that loads each plugin as JACK client. Suitable JACK client names are: `carla-jack-multi.nBAF/ZynAddSubFx` or `carla-jack-multi.nBAF/Helm` Please note that ZynAddSubFx and Helm are *not ports* but clients. Each of them can have any number of audio and midi ports below them. Note that this means that the application MUST NOT register with JACK (or any other subsystem requiring unique names) until it receives an `open` message from NSM. Likewise, applications with the `:switch:` capability should close their JACK clients and re-create them with using the new `client_id` (renaming JACK-clients is not possible, only ports). A response is REQUIRED as soon as the open operation has been completed. Ongoing progress MAY be indicated by sending messages to `/nsm/client/progress`. ===== Response The client MUST respond to the 'open' message with: [source%nowrap,OSC] ---- /reply "/nsm/client/open" s:message ---- Or [source%nowrap,OSC] ---- /error "/nsm/client/open" i:error_code s:message ---- .Response codes [options="header", stripes=even] |=== |Code | Meaning |ERR | General Error |ERR_BAD_PROJECT | An existing project file was found to be corrupt |ERR_CREATE_FAILED | A new project could not be created |ERR_UNSAVED_CHANGES | Unsaved changes would be lost |ERR_NOT_NOW | Operation cannot be completed at this time |=== ==== Save [source%nowrap,OSC] ---- /nsm/client/save ---- This message will only be delivered after a previous `open` message, and may be sent any number of times within the course of a session (including zero, if the user aborts the session). ===== Response [source%nowrap,OSC] ---- /reply "/nsm/client/save" s:message ---- Or [source%nowrap,OSC] ---- /error "/nsm/client/save" i:error_code s:message ---- .Response codes [options="header", stripes=even] |=== |Code | Meaning |ERR | General Error |ERR_SAVE_FAILED | Project could not be saved |ERR_NOT_NOW | Operation cannot be completed at this time |=== === Server to Client Informational Messages ==== Session is Loaded Accepting this message is optional. The intent is to signal to clients which may have some interdependence (say, peer to peer OSC connections) that the session is fully loaded and all their peers are available. Most clients will not need to act on this message. This message has no meaning when a session is being built or run; only when it is initially loaded. Clients who intend to act on this message MUST NOT do so by delaying initialization waiting for it. [source%nowrap,OSC] ---- /nsm/client/session_is_loaded ---- This message does not require a response. ==== Show Optional Gui If the client has specified the `optional-gui` capability, then it may receive this message from the server when the user wishes to change the visibility state of the GUI. It doesn't matter if the optional GUI is integrated with the program or if it is a separate program \(as is the case with SooperLooper\). When the GUI is hidden, there should be no window mapped and if the GUI is a separate program, it should be killed. [source%nowrap,OSC] ---- /nsm/client/show_optional_gui ---- [source%nowrap,OSC] ---- /nsm/client/hide_optional_gui ---- This message does not require a response. === Client to Server Informational Messages ==== Optional GUI If the client has specified the `optional-gui` capability, then it MUST send this message whenever the state of visibility of the optional GUI has changed. It also MUST send this message after its announce message to indicate the initial visibility state of the optional GUI. The client SHOULD always start hidden, if not saved as visible. That implies the first load, after adding to the session, SHOULD always be hidden. It is the responsibility of the client to remember the visibility state of its GUI across session loads. [source%nowrap,OSC] ---- /nsm/client/gui_is_hidden ---- [source%nowrap,OSC] ---- /nsm/client/gui_is_shown ---- No response will be delivered. ==== Progress [source%nowrap,OSC] ---- /nsm/client/progress f:progress ---- For potentially time-consuming operations, such as `save` and `open`, progress updates may be indicated throughout the duration by sending a floating point value between 0.0 and 1.0, 1.0 indicating completion, to the NSM server. The server will not send a response to these messages, but will relay the information to the user. Note that even when using the `progress` feature, the final response to the `save` or `open` message is still REQUIRED. Clients which intend to send progress messages MUST include `:progress:` in their `announce` capability string. ==== Dirtiness [source%nowrap,OSC] ---- /nsm/client/is_dirty ---- [source%nowrap,OSC] ---- /nsm/client/is_clean ---- Some clients may be able to inform the server when they have unsaved changes pending. Such clients may optionally send `is_dirty` and `is_clean` messages. Clients which have and use this capability MUST include `:dirty:` in their `announce` capability string. ==== Status Messsages [source%nowrap,OSC] ---- /nsm/client/message i:priority s:message ---- Clients may send miscellaneous status updates to the server for possible display to the user. This may simply be chatter that is normally written to the console. `priority` MUST be a number from 0 to 3, 3 being the most important. Clients which have and use this capability MUST include `:message:` in their `announce` capability string. === Error Code Definitions .Error Code Definitions [options="header", stripes=even] |=== |Symbolic Name | Integer Value |ERR_GENERAL | -1 |ERR_INCOMPATIBLE_API | -2 |ERR_BLACKLISTED | -3 |ERR_LAUNCH_FAILED | -4 |ERR_NO_SUCH_FILE | -5 |ERR_NO_SESSION_OPEN | -6 |ERR_UNSAVED_CHANGES | -7 |ERR_NOT_NOW | -8 |ERR_BAD_PROJECT | -9 |ERR_CREATE_FAILED | -10 |=== === Client to Server Control If the server publishes the `:server-control:` capability, then clients can also initiate action by the server. For example, a client might implement a 'Save All' option which sends a `/nsm/server/save` message to the server, rather than requiring the user to switch to the session management interface to effect the save. === Server Control API The session manager not only manages clients via OSC, but it is itself controlled via OSC messages. The server responds to the following messages. All of the following messages will be responded to, at the sender's address, with one of the two following messages: [source%nowrap,OSC] ---- /reply s:path s:message ---- [source%nowrap,OSC] ---- /error s:path i:error_code s:message ---- The first parameter of the reply is the path to the message being replied to. The `/error` reply includes an integer error code (non-zero indicates error). `message` will be a description of the error. The possible errors are: .Responses [options="header", stripes=even] |=== |Code |Meaning |ERR_GENERAL | General Error |ERR_LAUNCH_FAILED | Launch failed |ERR_NO_SUCH_FILE | No such file |ERR_NO_SESSION | No session is open |ERR_UNSAVED_CHANGES | Unsaved changes would be lost |=== * `/nsm/server/add s:executable_name` ** Adds a client to the current session. * `/nsm/server/save` ** Saves the current session. * `/nsm/server/open s:project_name` ** Saves the current session and loads a new session. * `/nsm/server/new s:project_name` ** Saves the current session and creates a new session. * `/nsm/server/duplicate s:new_project` ** Saves and closes the current session, makes a copy, and opens it. * `/nsm/server/close` ** Saves and closes the current session. * `/nsm/server/abort` ** Closes the current session WITHOUT SAVING * `/nsm/server/quit` ** Saves and closes the current session and terminates the server. * `/nsm/server/list` ** Lists available projects. One `/reply` message will be sent for each existing project. ** Afer listing the last session one final `/reply` with `/nsm/server/list, ""` will be send. That is an empty string. ==== Client to Client Communication If the server includes `:broadcast:` in its capability string, then clients may send broadcast messages to each other through the NSM server. Clients may send messages to the server at the path `/nsm/server/broadcast`. The format of this message is as follows: [source%nowrap,OSC] ---- /nsm/server/broadcast s:path [arguments...] ---- The message will then be relayed to all clients in the session at the path `path` (with the arguments shifted by one). For example the message: [source%nowrap,OSC] ---- /nsm/server/broadcast /tempomap/update "0,120,4/4:12351234,240,4/4" ---- Would broadcast the following message to all clients in the session (except for the sender), some of which might respond to the message by updating their own tempo maps. [source%nowrap,OSC] ---- /tempomap/update "0,120,4/4:12351234,240,4/4" ---- The Non programs use this feature to establish peer to peer OSC communication by symbolic names (client IDs) without having to remember the OSC URLs of peers across sessions. == API Versions and Behaviour Changes Here we will document all technical changes or differences in behaviour together with their API and project version numbers. The term "original" refers to Non Session Manager and "new" refers to New Session Manager. Version numbers follow link:https://semver.org/spec/v2.0.0.html[Semantic Versioning 2.0.0] .Semantic Versioning Scheme ``` Given a version number MAJOR.MINOR.PATCH, increment the: MAJOR version when you make incompatible API changes, MINOR version when you add functionality in a backwards compatible manner, and PATCH version when you make backwards compatible bug fixes. ``` .NSM Version Numbers [options="header", stripes=even] |=== |Subject | Version |Non Session Manager at moment of fork | 1.2 (June 2020) |Non Session Manager API | 1.0 link:https://github.com/original-male/non/blob/master/session-manager/src/nsmd.C[NON nsmd.C] |Original API Document | 1.0 link:http://non.tuxfamily.org/nsm/API.html[non.tuxfamily.org/nsm/API.html] |New Session Manager | 1.6.0 |New Session Manager API | 1.1.2 link:https://github.com/jackaudio/new-session-manager/blob/master/src/nsmd.cpp[NEW nsmd.cpp] |New API Document | 1.5.0 link:#[Here] |=== === Guidelines The most important factor in decision making is to keep client compatibility at 100%. No client will ever receive an unrequested OSC message except those in API 1.0.0. Messages that drastically change existing `/nsm/client/` or `/nsm/server` behaviour require an inrecement to `API_VERSION_MAJOR`, which we want to avoid. `nsmd` checks if the clients `API_VERSION_MAJOR` is greater than its own and refuses the client with `ERR_INCOMPATIBLE_API`. All changes (that concern client/server behaviour) that increment `API_VERSION_MINOR` will be request-only or gated by new capabilities (e.g. `:optional-gui:`). `nsmd` will not send any messages if a capability was not sent by the client in <<Announce,`announce`>>. This includes mostly optional features about requesting extra information. New actions for server-control, for example a hypothetical `/nsm/server/save_as`, which would be triggered by the client and would only be *answered* by the server ("no unrequested message") will increment `API_VERSION_MINOR`. All changes that increment `API_VERSION_PATCH` will not have any effect on behaviour, except to fix clear problems, where "problem" is defined by having a different effect than described in this document, which includes technical problems such as crashes. All messages regarding GUI-communication that start with `/nsm/gui/...` were undocumented in API 1.0.0 and only used by `non-session-manager` / `nsm-legacy-gui`. Until properly documented in this document this part of the API is considered unstable and may change at any time without notice. However, when changing already existing messages and behaviour it MAY increment `API_VERSION_MINOR` or `API_VERSION_PATCH`. In that case it will appear in the list below. Last factor of compatibility is that any unknown message sent to `nsmd` will just print a warning message to stdout, but will otherwise be ignored. This secures a stable server, even when a client misbehaves and sends too-new messages outside of announced :capabilites: === Changes in API Version 1.1.0 Rewritten API document without code changes to adapt to existing code or existing client behaviour: * Changed versioning scheme to Semantic Versioning with three positions Major.Minor.Patch * <<Quit or Exit>> SHOULD hide instead of exiting when :optional-gui: is supported and MAY not act on the quit through menu otherwise. * <<#server-to-client-control-messages-open,Open>>: Make clear that there are only certain possibilities for save paths. We added MUST because the rule was just implied before. * <<#server-to-client-control-messages-open,Open>>: Make clear that the delimiter for multi-jack clients is "/". * <<Optional GUI>> SHOULD start hidden, always after a fresh add to the session. After that saving the visibility state may override it for next time. * <<Progress>> MUST be announced in :capabilities: . Before there was a lower case "should", which means nothing. Parallel-examples in the specs cleary say that supporting optional features must be announced first. ** Same for <<Dirtiness>> and <<Status Messsages>>. * <<Status Messsages>> have priority numbers between 0 and 3, so they MUST send that. It was never an arbitrary value. Code changes: * <<Server Control API>>: `/nsm/server/list` chain of single OSC messages, one for each session, is now finalized with sending and empty string "" as session name. Previously this was just a symbolically irrelevant console message `"Done."` * Replies to `/nsm/server/save` etc. will now be sent back to the sender and not falsely to the last client who replied to `/nsm/client/save`. This alone would only require API_VERSION_PATCH increment, but we are already incrementing minor. * <<Server Control API>>: `/nsm/server/add` was replying with an undocumented error code on success. Instead, as this document always specificed, it now sends `"/reply", path, "Launched."`. Again, this would have been just API_VERSION_PATCH on its own. Undocumented (Unstable) `/nsm/gui` protocol * Send client status after a GUI attaches to running server. This was not happening before, but it was the intention. It was just broken in nsmd.cpp. This alone would only require API_VERSION_PATCH increment, but we are already incrementing minor. * Send label "launch error!" when a program is added (or loaded) that does not exist in $PATH. This requires no adaptation of any client, server or GUI because labels are arbitrary already and this is not meant for automatic parsing, but as user information. * `/nsm/gui/session/name` will now always send the same parameter format, regardless of how the session was opened: simple-session-name, relative session path with subdirs below session-root. * When a GUI announces itself to nsmd it will receive the absolute path to the session directory through the message `/nsm/gui/session/root`. This is not a new addition but was already in non-session-manager git. === Changes in API Version 1.1.1 * Server-capability :optional-gui: is now mandatory for SERVER implementations. Reasoning: This is an important core feature of NSM and thus will be treated as such by guaranteeing it to exist. After looking at all currently known clients and server-implementations it turns out that all servers support :optional-gui: and the vast majority of clients not only support it, but actually assume it and do _not_ test for the server capability, as it was written in this document. There are now two choices: Adjust this document to the (good) reality or consider all clients broken. Summary: We consider this API document wrong and therefore fix it, thus increasing API version patch-level from 1.1.0 to 1.1.1 * Add API-section "Subdirectories / Hierarchical Structure" that explains the session directory. This behaviour was already the case for nsm-legacy-gui and nsmd 1.5.0 was patched to adhere to this behaviour more strictly as well, removing false session entries in 3rd party clients such as Agordejo. === Changes in API Version 1.1.2 * nsmd now follows the XDG Base Directory Specifications for it's session root and lock files. This if of no consequence to clients but required documentation nevertheless, which was described as "background information" in the chapters for lock files and daemon disovery. * nsmd now gracefully handles read-only `session.nsm` files. This theoretically enables read-only sessions and session-templates. It is included in the patch-level because this was marked as a long-standing `FIXME` in the code by the original author. Or in other words: just a bug fix. 0707010000000F000081A40000000000000000000000016258A65C000003EA000000000000000000000000000000000000004700000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/api/readme.txtThe statements made here only concern the API document. All other documentation and help texts of the "New Session Manager" are original works. This subdirectory "API" contains an adapted copy of http://non.tuxfamily.org/nsm/API.html, which was released by Jonathan Moore Liles <male@tuxfamily.org> under the title "Non Session Management API Version 1." It was published on the the NON-Website on 2013-04-06 with git commit d7cf8955b8557ccc56d108425a2c61b0e1ac73f4 under the Creative Commons By-Sa License 2.5, as was the whole website (see website footer). For a full copy of the license please see the attached file LICENSE. This adaption and all changes are licensed under CC-By-Sa 2.5 as well. The original work was copied from the NON-website by hand in 2020-07 by Nils Hilbricht. Initial git commit on 2020-07-06 is the unaltered content by Jonathan Moore Liles, not counting layout changes because the document format changed. All subsequent changes and authors can be reviewed by git log. 07070100000010000081ED0000000000000000000000016258A65C00001095000000000000000000000000000000000000004400000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/generate.sh#!/bin/sh #The documentation is built statically and does not belong to the normal build process. #Updating is part of the development process, not compiling or packaging. #Run this before a release, or any time you want to update the docs or the README. #This script takes common snippets of information and updates or generates source info files from #them. # parse src/nsmd.cpp for API version, insert into /docs/src/api/index.adoc # parse src/nsmd.cpp for package version, insert into /meson.build and /docs/src/index.adoc # generate /README.md (shares text with manual index) # generate manpages # convert all .adoc files to html in /docs/ (This enables github to directly present this dir as website) # # WARNING: You still need to manually edit the date and version in /CHANGELOG # #We do _not_ change the copyright date in files license-headers. #They only exist to mark to year of the fork. In the future dates might be removed completely. set -e #Stop script on errors set -u #Trace unset variables as an error. #Change pwd to root dir parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) cd "$parent_path"/../.. [ -f "CHANGELOG" ] || ( echo "not in the root dir"; exit 1 ) #assert correct dir [ -f "build/nsmd" ] || ( echo "no build/ dir with binaries"; exit 1 ) #assert build was made, for manpages #Gather data ROOT=$(pwd) #save for later VERSION=$(grep "define VERSION_STRING" "src/nsmd.cpp" | cut -d ' ' -f 3) #Get version as "1.4" string VERSION="${VERSION%\"}" #Remove " VERSION="${VERSION#\"}" #Remove " _MAJORAPI=$(grep "define NSM_API_VERSION_MAJOR" "src/nsmd.cpp" | cut -d ' ' -f 3) _MINORAPI=$(grep "define NSM_API_VERSION_MINOR" "src/nsmd.cpp" | cut -d ' ' -f 3) _PATCHAPI=$(grep "define NSM_API_VERSION_PATCH" "src/nsmd.cpp" | cut -d ' ' -f 3) APIVERSION=$_MAJORAPI"."$_MINORAPI"."$_PATCHAPI #Present data to confirm write-action echo "Root: $ROOT" echo "Version: $VERSION" echo "API Version: $APIVERSION" echo "Please make sure that your meson build dir is up to date for manpage generation" read -p "Is parsed data correct? Continue by writing files? [y|n] " -n 1 -r if [[ ! $REPLY =~ ^[Yy]$ ]] then echo echo "Abort" exit 1 fi echo echo echo "Update meson.build version number" cd "$ROOT" sed -i "/^version :.*/c\version : '$VERSION'," meson.build #Find the version line and replace with entire new line echo "Update docs to programs version number" cd "$ROOT/docs/src" sed -i '/^\:revnumber.*/c\:revnumber: '$VERSION index.adoc #Find the revnumber line and replace with entire new line echo "Update API document to API version number" cd "$ROOT/docs/src/api" sed -i '/^\:revnumber.*/c\:revnumber: API '$APIVERSION index.adoc #Find the revnumber line and replace with entire new line echo "Generate README from snippets" cd "$ROOT/docs/src" cat "readme-00.md" "readme-01.md" "readme-02.md" > "$ROOT/README.md" echo "Generate website and documentation with Asciidoctor using README snippets" echo " We generate directly into docs/ and not into e.g. docs/out because github can read docs/ directly." cd "$ROOT/docs/" mkdir -p "api" asciidoctor src/index.adoc -o index.html asciidoctor src/api/index.adoc -o api/index.html echo "Generate all manpages" cd "$ROOT/docs/src" #We tested earlier that a build-dir exists help2man ../../build/nsmd --version-string="nsmd Version $VERSION" --no-info --include manpage-common.h2m > nsmd.1 help2man ../../build/nsm-legacy-gui --version-string="nsm-legacy-gui Version $VERSION" --no-info --include manpage-common.h2m > nsm-legacy-gui.1 help2man ../../build/nsm-legacy-gui --version-string="nsm-legacy-gui Version $VERSION" --no-info --include manpage-common.h2m > non-session-manager.1 help2man ../../build/nsm-proxy --version-string="nsm-proxy Version $VERSION" --no-info --include manpage-common.h2m > nsm-proxy.1 help2man ../../build/nsm-proxy-gui --version-string="nsm-proxy-gui Version $VERSION" --no-info --include manpage-common.h2m > nsm-proxy-gui.1 help2man ../../build/jackpatch --version-string="jackpatch Version $VERSION" --no-info --include manpage-common.h2m > jackpatch.1 echo echo "Don't forget to adjust the version and date in CHANGELOG manually." echo "Finished. You need to commit your changes to git manually." 07070100000011000081A40000000000000000000000016258A65C00003AFC000000000000000000000000000000000000004200000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/icons.svg<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="210mm" height="297mm" viewBox="0 0 210 297" version="1.1" id="svg8" sodipodi:docname="icons.svg" inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> <defs id="defs2" /> <sodipodi:namedview units="px" id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="-962.06585" inkscape:cy="-215.33923" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" showgrid="false" inkscape:window-width="2558" inkscape:window-height="1398" inkscape:window-x="0" inkscape:window-y="20" inkscape:window-maximized="1" /> <metadata id="metadata5"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"> <g id="nsm" inkscape:label="nsm" transform="translate(-172.56864)"> <title id="title888">nsm</title> <rect y="114.63333" x="71.133331" height="67.73333" width="67.73333" id="rect10" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="105.32493" y="169.25755" id="text852"><tspan sodipodi:role="line" x="105.32493" y="169.25755" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan850">NSM</tspan></text> </g> <g transform="translate(-170.13191,80.919145)" inkscape:label="jackpatch" id="jackpatch"> <title id="title890">jackpatch</title> <rect style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect892" width="67.73333" height="67.73333" x="71.133331" y="114.63333" /> <text id="text896" y="165.8237" x="106.38719" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" xml:space="preserve"><tspan id="tspan894" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" y="165.8237" x="106.38719" sodipodi:role="line">JP</tspan></text> </g> <g id="proxy" inkscape:label="proxy"> <title id="title940">proxy</title> <rect y="37.223072" x="-99.065941" height="67.73333" width="67.73333" id="rect902" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="-65.639175" y="93.759422" id="text906"><tspan sodipodi:role="line" x="-65.639175" y="93.759422" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan904">PRX</tspan></text> </g> <path d="m 231.64549,37.223072 h 67.73333 V 104.9564 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1679" /> <g style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="text1683" aria-label="PRX"> <path id="path1690" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 238.98532,48.420053 0.0677,45.339369 h 4.87229 V 72.578493 l 0.27069,-0.06767 h 4.60161 c 1.42108,0 2.63915,-0.406024 3.65421,-1.150401 1.01506,-0.744378 1.48876,-1.962451 1.48876,-3.654218 V 53.427685 c 0,-1.96245 -0.4737,-3.315864 -1.42109,-3.992571 -0.94739,-0.676707 -2.16546,-1.015061 -3.72188,-1.015061 z m 4.93996,4.872291 h 3.31587 c 0.47369,0 0.81205,0.135341 1.1504,0.473695 0.33835,0.338353 0.4737,0.744377 0.4737,1.218072 v 10.962654 c 0,0.473694 -0.13535,0.812048 -0.4737,1.150401 -0.33835,0.338354 -0.67671,0.473695 -1.1504,0.473695 h -3.31587 z" /> <path id="path1692" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 262.66997,53.292344 h 3.31586 c 0.4737,0 0.81205,0.135341 1.15041,0.473695 0.33835,0.338353 0.47369,0.744377 0.47369,1.218072 v 10.962654 c 0,0.473694 -0.13534,0.812048 -0.47369,1.150401 -0.33836,0.338354 -0.67671,0.473695 -1.15041,0.473695 h -3.31586 z m -4.93996,-4.872291 0.0677,45.339369 h 4.87229 v -21.2486 h 1.21807 l 4.19559,21.2486 h 4.87229 L 268.69266,72.24014 c 0.60904,-0.203012 1.42109,-0.609037 2.50382,-1.218073 0.94739,-0.609036 1.48875,-1.759438 1.48875,-3.451206 V 53.427685 c 0,-1.285743 -0.47369,-2.436145 -1.48875,-3.518876 -1.01506,-1.015061 -2.16547,-1.488756 -3.51888,-1.488756 z" /> <path id="path1694" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 275.6627,48.420053 5.0753,23.346392 -4.80462,21.789965 h 5.00763 l 2.90984,-13.53414 2.90984,13.53414 h 4.93996 l -4.73695,-21.789965 5.07531,-23.346392 h -5.14298 l -3.04518,14.752213 -3.04518,-14.752213 z" /> </g> <path d="m 230.75774,195.55247 h 67.73333 v 67.73333 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1700" /> <path d="m 256.70029,252.35951 v -50.95604 h -5.0753 v 49.46729 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -1.75944 v 4.93996 h 3.04518 c 3.58655,0 5.41366,-1.69176 5.41366,-5.0753 z" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;line-height:1.25;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="path1712" /> <path d="m 266.05212,201.40347 0.0677,45.33937 h 4.87229 v -21.18093 l 0.27068,-0.0677 h 4.60161 c 1.42108,0 2.63916,-0.40602 3.65422,-1.1504 1.01506,-0.74438 1.48875,-1.96245 1.48875,-3.65422 v -14.27851 c 0,-1.96245 -0.47369,-3.31587 -1.42108,-3.99257 -0.94739,-0.67671 -2.16546,-1.01507 -3.72189,-1.01507 z m 4.93996,4.87229 h 3.31586 c 0.4737,0 0.81205,0.13535 1.1504,0.4737 0.33836,0.33835 0.4737,0.74438 0.4737,1.21807 v 10.96266 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -3.31586 z" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;line-height:1.25;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="path1714" /> <path d="m 232.98119,114.63333 h 67.73333 v 67.73333 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1875" /> <g style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="text1879" aria-label="NSM"> <path id="path1886" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 236.41654,123.6475 0.13535,0.27068 v 45.33937 h 5.00763 v -29.70743 l 8.79719,29.70743 h 5.34598 l -0.13534,-0.40602 V 123.6475 h -5.21064 v 29.63977 l -8.72952,-29.63977 z" /> <path id="path1888" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 269.64283,169.52824 c 1.35341,0 2.50382,-0.4737 3.45121,-1.35342 1.01506,-0.94739 1.48875,-2.09779 1.48875,-3.4512 v -14.48153 c 0,-1.28575 -0.47369,-2.43615 -1.35341,-3.45121 -0.94739,-0.94739 -2.16546,-1.6241 -3.58655,-2.09779 l -3.65422,-1.1504 c -0.40602,-0.13534 -0.81205,-0.4737 -1.1504,-0.94739 -0.33835,-0.54137 -0.47369,-1.01506 -0.47369,-1.48876 v -10.82731 c 0,-0.4737 0.13534,-0.81205 0.47369,-1.1504 0.33835,-0.33836 0.67671,-0.4737 1.1504,-0.4737 h 1.6241 c 0.47369,0 0.81205,0.13534 1.1504,0.4737 0.33836,0.33835 0.4737,0.6767 0.4737,1.1504 v 3.51888 h 5.00763 v -5.27832 c 0,-1.42108 -0.4737,-2.57148 -1.42109,-3.51887 -1.01506,-0.94739 -2.16546,-1.42109 -3.51887,-1.42109 h -4.93996 c -1.42109,0 -2.57149,0.4737 -3.51888,1.42109 -0.94739,0.94739 -1.42108,2.09779 -1.42108,3.51887 v 14.21085 c 0,1.28574 0.47369,2.50381 1.35341,3.51888 0.94739,1.01506 2.16546,1.69176 3.58655,2.16546 l 3.65421,1.08273 c 0.4737,0.13534 0.81205,0.40602 1.15041,0.87972 0.33835,0.47369 0.47369,0.87972 0.47369,1.35341 v 11.16567 c 0,0.47369 -0.13534,0.87972 -0.47369,1.21807 -0.33836,0.33835 -0.67671,0.47369 -1.15041,0.47369 h -1.62409 c -0.4737,0 -0.81205,-0.13534 -1.1504,-0.47369 -0.33836,-0.33835 -0.4737,-0.74438 -0.4737,-1.21807 v -3.51888 h -5.00763 v 5.34599 c 0,1.35341 0.47369,2.50381 1.42108,3.4512 1.01506,0.87972 2.16547,1.35342 3.51888,1.35342 z" /> <path id="path1890" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 278.30458,123.91818 0.13534,45.33937 h 5.14298 v -31.94057 l 4.19558,15.97029 h 0.67671 l 3.99257,-15.97029 v 31.94057 h 5.27831 v -45.33937 h -4.80462 l -4.80462,15.0229 -4.87229,-15.0229 z" /> </g> <text id="text1896" y="14.555378" x="-90.511452" style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583" xml:space="preserve"><tspan style="stroke-width:0.264583" y="14.555378" x="-90.511452" id="tspan1894" sodipodi:role="line">Texts and</tspan><tspan id="tspan1898" style="stroke-width:0.264583" y="27.784504" x="-90.511452" sodipodi:role="line">Objects</tspan></text> <text id="text1902" y="8.2739525" x="239.56985" style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583" xml:space="preserve"><tspan style="stroke-width:0.264583" y="8.2739525" x="239.56985" id="tspan1900" sodipodi:role="line">Converted</tspan><tspan id="tspan1904" style="stroke-width:0.264583" y="21.503078" x="239.56985" sodipodi:role="line">to paths</tspan></text> <text id="text1908" y="31.889189" x="27.74991" style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583" xml:space="preserve"><tspan style="stroke-width:0.264583" y="31.889189" x="27.74991" id="tspan1906" sodipodi:role="line">Inkscape export single icon:</tspan><tspan id="tspan1994" style="stroke-width:0.264583" y="45.118317" x="27.74991" sodipodi:role="line">Select Icon, Ctrl+Shift+R</tspan><tspan id="tspan1910" style="stroke-width:0.264583" y="58.347443" x="27.74991" sodipodi:role="line">to resize to selection.</tspan><tspan id="tspan1912" style="stroke-width:0.264583" y="71.576561" x="27.74991" sodipodi:role="line">File -> Save a Copy </tspan><tspan id="tspan1914" style="stroke-width:0.264583" y="84.805687" x="27.74991" sodipodi:role="line">Inkscape SVG works as icon</tspan></text> </g> </svg> 07070100000012000081A40000000000000000000000016258A65C000003C2000000000000000000000000000000000000004300000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/index.adoc//// This is "asciidoctor", not plain "asciidoc". https://asciidoctor.org/docs/user-manual/ //// //// This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. A copy of the license has been provided in the file documentation/LICENSE. //// :Author: Nils Hilbricht :iconfont-remote!: :!webfonts: :revnumber: 1.6.0 = New Session Manager Documentation * link:https://github.com/jackaudio/new-session-manager[Sourcecode] * link:https://github.com/jackaudio/new-session-manager/issues[Bug and Issue Tracker] * link:api/index.html[API] document that describes all OSC Messages * link:http://non.tuxfamily.org/session-manager/doc/MANUAL.html[Legacy-GUI Manual]. The original Non-Session-Manager GUI manual is still valid. include::readme-01.md[] 07070100000013000081A40000000000000000000000016258A65C000004F2000000000000000000000000000000000000004400000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/jackpatch.1.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. .TH JACKPATCH "1" "April 2022" "jackpatch Version 1.6.0" "User Commands" .SH NAME jackpatch \- manual page for jackpatch Version 1.6.0 .SH DESCRIPTION jackpatch \- Remember and restore the JACK Audio Connection Kit Graph in NSM .PP It is intended as module for the 'New Session Manager' and only communicates over OSC in an NSM\-Session. .PP It has limited standalone functionality for testing and debugging, such as: .SS "Usage:" .TP jackpatch [filename] Restore a snapshot from \fB\-\-save\fR and monitor. .IP jackpatch \fB\-\-help\fR .SH OPTIONS .TP \fB\-\-help\fR Show this screen and exit .TP \fB\-\-version\fR Show version and exit .TP \fB\-\-save\fR Save current connection snapshot to file and exit .SH "REPORTING BUGS" https://github.com/jackaudio/new-session-manager/issues .SH COPYRIGHT up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org .SH "SEE ALSO" The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 07070100000014000081A40000000000000000000000016258A65C000002D3000000000000000000000000000000000000004600000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/jackpatch.h2m [name] jackpatch - JACK Audio Connection Kit environment saver for the "New Session Manager" [usage] jackapatch is a module for "New Session Manager". It only communicates over OSC in an NSM-Session and has no standalone functionality. [Reporting bugs] https://github.com/jackaudio/new-session-manager/issues [copyright] up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org [see also] The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 07070100000015000081A40000000000000000000000016258A65C000001E5000000000000000000000000000000000000004B00000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/manpage-common.h2m [Reporting bugs] https://github.com/jackaudio/new-session-manager/issues [copyright] up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org [see also] The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 07070100000016000081A40000000000000000000000016258A65C000004BC000000000000000000000000000000000000004E00000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/non-session-manager.1.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. .TH NSM-LEGACY-GUI "1" "April 2022" "nsm-legacy-gui Version 1.6.0" "User Commands" .SH NAME nsm-legacy-gui \- manual page for nsm-legacy-gui Version 1.6.0 .SH DESCRIPTION nsm\-legacy\-gui \- FLTK GUI for the 'New Session Manager' .SS "Usage:" .IP nsm\-legacy\-gui nsm\-legacy\-gui \fB\-\-help\fR .SH OPTIONS .TP \fB\-\-help\fR Show this screen .TP \fB\-\-nsm\-url\fR url Connect to a running nsmd [Example: osc.udp://mycomputer.localdomain:38356/]. .TP \fB\-\-\fR Everything after \fB\-\-\fR will be given to nsmd as server options. See nsmd \fB\-\-help\fR . .PP For backwards compatibility this executable also exist as symlink 'non\-session\-manager' .SH "REPORTING BUGS" https://github.com/jackaudio/new-session-manager/issues .SH COPYRIGHT up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org .SH "SEE ALSO" The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 07070100000017000081A40000000000000000000000016258A65C000004BC000000000000000000000000000000000000004900000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/nsm-legacy-gui.1.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. .TH NSM-LEGACY-GUI "1" "April 2022" "nsm-legacy-gui Version 1.6.0" "User Commands" .SH NAME nsm-legacy-gui \- manual page for nsm-legacy-gui Version 1.6.0 .SH DESCRIPTION nsm\-legacy\-gui \- FLTK GUI for the 'New Session Manager' .SS "Usage:" .IP nsm\-legacy\-gui nsm\-legacy\-gui \fB\-\-help\fR .SH OPTIONS .TP \fB\-\-help\fR Show this screen .TP \fB\-\-nsm\-url\fR url Connect to a running nsmd [Example: osc.udp://mycomputer.localdomain:38356/]. .TP \fB\-\-\fR Everything after \fB\-\-\fR will be given to nsmd as server options. See nsmd \fB\-\-help\fR . .PP For backwards compatibility this executable also exist as symlink 'non\-session\-manager' .SH "REPORTING BUGS" https://github.com/jackaudio/new-session-manager/issues .SH COPYRIGHT up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org .SH "SEE ALSO" The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 07070100000018000081A40000000000000000000000016258A65C0000047A000000000000000000000000000000000000004800000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/nsm-proxy-gui.1.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. .TH NSM-PROXY-GUI "1" "April 2022" "nsm-proxy-gui Version 1.6.0" "User Commands" .SH NAME nsm-proxy-gui \- manual page for nsm-proxy-gui Version 1.6.0 .SH DESCRIPTION nsm\-proxy\-gui \- GUI for nsm\-proxy, a wrapper for executables without direct NSM\-Support. .SS "Usage:" .IP nsm\-proxy\-gui \fB\-\-help\fR nsm\-proxy\-gui \fB\-\-connect\-to\fR .SH OPTIONS .TP \fB\-\-help\fR Show this screen .TP \fB\-\-connect\-to\fR Connect to running nsm\-proxy .PP nsmd\-proxy\-gui is usually not called by the user directly, but autostarted when nsm\-proxy is added to a session (through a GUI). .SH "REPORTING BUGS" https://github.com/jackaudio/new-session-manager/issues .SH COPYRIGHT up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org .SH "SEE ALSO" The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 07070100000019000081A40000000000000000000000016258A65C000003EC000000000000000000000000000000000000004400000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/nsm-proxy.1.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. .TH NSM-PROXY "1" "April 2022" "nsm-proxy Version 1.6.0" "User Commands" .SH NAME nsm-proxy \- manual page for nsm-proxy Version 1.6.0 .SH DESCRIPTION nsm\-proxy \- Wrapper for executables without direct NSM\-Support. .PP It is a module for the 'New Session Manager' and only communicates over OSC in an NSM\-Session and has no standalone functionality. .SS "Usage:" .IP nsm\-proxy \fB\-\-help\fR .SH OPTIONS .TP \fB\-\-help\fR Show this screen .SH "REPORTING BUGS" https://github.com/jackaudio/new-session-manager/issues .SH COPYRIGHT up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org .SH "SEE ALSO" The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 0707010000001A000081A40000000000000000000000016258A65C000005F6000000000000000000000000000000000000003F00000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/nsmd.1.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. .TH NSMD "1" "April 2022" "nsmd Version 1.6.0" "User Commands" .SH NAME nsmd \- manual page for nsmd Version 1.6.0 .SH DESCRIPTION nsmd \- Daemon and server for the 'New Session Manager' .SS "Usage:" .IP nsmd nsmd \fB\-\-help\fR nsmd \fB\-\-version\fR .SH OPTIONS .TP \fB\-\-help\fR Show this screen .TP \fB\-\-version\fR Show version .TP \fB\-\-osc\-port\fR portnum OSC port number [Default: provided by system]. .TP \fB\-\-session\-root\fR path Base path for sessions [Default: $XDG_DATA_HOME/nsm/]. .TP \fB\-\-load\-session\fR name Load existing session [Example: "My Song"]. .TP \fB\-\-gui\-url\fR url Connect to running legacy\-gui [Example: osc.udp://mycomputer.localdomain:38356/]. .TP \fB\-\-detach\fR Detach from console. .TP \fB\-\-quiet\fR Suppress messages except warnings and errors. .PP nsmd can be run headless with existing sessions. To create new ones it is recommended to use a GUI such as nsm\-legacy\-gui (included) or Agordejo (separate package) .SH "REPORTING BUGS" https://github.com/jackaudio/new-session-manager/issues .SH COPYRIGHT up to 2020: Jonathan Moore Liles https://non.tuxfamily.org/ from 2020: Nils Hilbricht et al. https://new-session-manager.jackaudio.org .SH "SEE ALSO" The full documentation for NSM is maintained as html site in your systems doc-dir. For example: xdg-open file:///usr/share/doc/new-session-manager/index.html The documentation can also be found online https://new-session-manager.jackaudio.org 0707010000001B000081A40000000000000000000000016258A65C00000016000000000000000000000000000000000000004500000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/readme-00.md# New Session Manager 0707010000001C000081A40000000000000000000000016258A65C000009D0000000000000000000000000000000000000004500000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/readme-01.md ## Introduction New Session Manager (NSM) is a tool to assist music production by grouping standalone programs into sessions. Your workflow becomes easy to manage, robust and fast by leveraging the full potential of cooperative applications. NSM continues to be free in every sense of the word: free of cost, free to share and use, free of spyware or ads, free-and-open-source. You can create a session, or project, add programs to it and then use commands to save, start/stop, hide/show all programs at once, or individually. At a later date you can then re-open the session and continue where you left off. All files belonging to the session will be saved in the same directory. If you are a user (and not a programmer or packager) everything you need is to install NSM through your distributions package manager and, highly recommended, Agordejo as a GUI (see below). To learn NSM you don't need to know the background information from our documentation, which is aimed at developers that want to implement NSM support in their programs. Learn the GUI, not the server and protocol. ## Bullet Points * Drop-In replacement for the non-session-manager daemon nsmd and tools (e.g. jackpatch) * Simple and hassle-free build system to make packaging easy * Possibility to react to sensible bug fixes that would not have been integrated original nsmd * Stay upwards and downwards compatible with original nsmd * Conservative and hesitant in regards to new features and behaviour-changes, but possible in principle * Keep the session-manager separate from the other NON* tools Mixer, Sequencer and Timeline. * Protect nsmd from vanishing from the internet one day. * The goal is to become the de-facto standard music session manager for Linux distributions ## User Interface It is highly recommended to use Agordejo ( https://www.laborejo.org/agordejo/ ) as graphical user interface. In fact, if you install Agordejo in your distribution it will install NSM as dependency and you don't need to do anything yourself with this software package. This repository also contains the legacy FLTK interface simply called `nsm-legacy-gui`, symlinked to `non-session-manager` for backwards compatibility. (e.g. autostart scripts etc.) ## Supported Clients While NSM can start and stop any program it only becomes convenient if clients specifically implement support. This enables saving and hiding the GUI, amongst other features. Documentation and tutorials for software-developers will be added at a later date. 0707010000001D000081A40000000000000000000000016258A65C00000CC9000000000000000000000000000000000000004500000000new-session-manager-1.6.0+git.20220415.0f6719c/docs/src/readme-02.md ## Documentation and Manual Our documentation contains the API specification for the NSM protocol, which is the central document if you want to add NSM support to your own application. You can find html documentation installed to your systems SHARE dir or docs/out/index.html in this repository. There is also an online version https://jackaudio.github.io/new-session-manager/ We also provide a set of manpages for each executable (see Build). ## Fork and License This is a fork of non-session-manager, by Jonathan Moore Liles <male@tuxfamily.net> http://non.tuxfamily.org/ which was released under the GNU GENERAL PUBLIC LICENSE Version 2, June 1991. All files, except nsm.h kept in this fork were GPL "version 2 of the License, or (at your option) any later version." `nsm.h` is licensed under the ISC License. New-Session-Manager changed the license to GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007. See file COPYING Documentation in docs/ is licensed Creative Commons CC-By-Sa. It consist of mostly generated files and snippet files which do not have a license header for technical reasons. All original documentation source files are CC-By-Sa Version v4.0 (see file docs/src/LICENSE), the source file docs/src/api/index.adoc is a derived work from NON-Session-Managers API file which is licensed CC-By-Sa v2.5. Therefore our derived API document is also CC-By-Sa v2.5 (see files docs/src/api/readme.txt and docs/src/api/LICENSE) ## Build The build system is meson. This is a software package that will compile and install multiple executables: * `nsmd`, the daemon or server itself. It is mandatory. * It has no GUI. * Dependency is `liblo`, the OSC library. * `jackpatch`, NSM client to save and remember JACK connections. * It has no GUI. * Dependencies are `JACK Audio Connection Kit` and `liblo`, the OSC library. * Can be deactivated (see below) `-Djackpatch=false` * `nsm-legacy-gui`, Legacy GUI for the user * Formerly known as "non-session-manager" * Dependencies are `FLTK`>=v1.3.0 and `liblo`, the OSC library. * Can be deactivated (see below) `-Dlegacy-gui=false` * `nsm-proxy`, NSM GUI Client to run any program without direct NSM support * Dependencies are `FLTK`>=v1.3.0, `fluid` (FLTK Editor/compiler, maybe in the same package as FLTK, maybe not) and `liblo`, the OSC library. * Can be deactivated (see below) `-Dnsm-proxy=false` ``` meson build --prefix=/usr #or disable individual build targets: #meson build --prefix=/usr -Dlegacy-gui=false -Dnsm-proxy=false -Djackpatch=false cd build && ninja sudo ninja install ``` Optionally you can skip `sudo ninja install` and run all executables from the build-dir. In this case you need to add the build-dir to your PATH environment variable so that the tools can find each other. ## Names of Executable Files and Symlinks Some distributions (and possibly local laws) prevent a forked software project from creating executable files under the same name, if the name itself is an original work subject to copyright, which it arguably is for the "NON-"-suite. Therefore New Session Manager renamed `non-session-manager` to `nsm-legacy-gui`. Installing will also create a symlink to `non-session-manager` for backwards compatibility. (e.g. autostart scripts etc.). 0707010000001E000041ED0000000000000000000000046258A65C00000000000000000000000000000000000000000000003600000000new-session-manager-1.6.0+git.20220415.0f6719c/extras0707010000001F000081A40000000000000000000000016258A65C00000222000000000000000000000000000000000000004000000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/README.md# New Session Manager - Extras Each subdirectory in /extras holds additional libraries, software, documentation etc. They are included for convenience, e.g. the library pynsm is useful for client-programmers, and also developed by the same author as New-Session-Manager. Each "extra" is standalone regarding license and build process. They are not build or installed through nsm(d) meson build. The main programs in this repository do not depend on files in /extra in any way. From a technical point of view `/extras` could be safely deleted. 07070100000020000041ED0000000000000000000000026258A65C00000000000000000000000000000000000000000000003C00000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/nsm.h07070100000021000081A40000000000000000000000016258A65C000002CE000000000000000000000000000000000000004400000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/nsm.h/COPYINGhttps://www.isc.org/licenses/ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED “AS IS” AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 07070100000022000081A40000000000000000000000016258A65C00005E89000000000000000000000000000000000000004200000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/nsm.h/nsm.h /*******************************************A******************************/ /* Copyright (C) 2012 Jonathan Moore Liles */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* Permission to use, copy, modify, and/or distribute this software for */ /* any purpose with or without fee is hereby granted, provided that the */ /* above copyright notice and this permission notice appear in all */ /* copies. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL */ /* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED */ /* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE */ /* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL */ /* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR */ /* PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER */ /* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR */ /* PERFORMANCE OF THIS SOFTWARE. */ /*************************************************************************/ /*************************************************************************/ /* A simple, callback based C API for NSM clients. */ /* */ /* Simplified Example: */ /* */ /* #include "nsm.h" */ /* */ /* static nsm_client_t *nsm = 0; */ /* static int wait_nsm = 1; */ /* */ /* int */ /* cb_nsm_open ( const char *save_file_path, //See API Docs 2.2.2 */ /* const char *display_name, //Not useful */ /* const char *client_id, //Use as JACK Client Name */ /* char **out_msg, */ /* void *userdata ) */ /* { */ /* do_open_stuff(); //Your own function */ /* wait_nsm = 0; */ /* return ERR_OK; */ /* } */ /* */ /* int */ /* cb_nsm_save ( char **out_msg, */ /* void *userdata ) */ /* { */ /* do_save_stuff(); //Your own function */ /* return ERR_OK; */ /* } */ /* */ /* void */ /* cb_nsm_show ( void *userdata ) */ /* { */ /* do_show_ui(); //Your own function */ /* nsm_send_is_shown ( nsm ); */ /* } */ /* */ /* void */ /* cb_nsm_hide ( void *userdata ) */ /* { */ /* do_hide_ui(); //Your own function */ /* nsm_send_is_hidden ( nsm ); */ /* } */ /* */ /* gboolean */ /* poll_nsm() */ /* { */ /* if ( nsm ) */ /* { */ /* nsm_check_nowait( nsm ); */ /* return true; */ /* } */ /* return false; */ /* } */ /* */ /* int main( int argc, char **argv ) */ /* { */ /* const char *nsm_url = getenv( "NSM_URL" ); */ /* */ /* if ( nsm_url ) */ /* { */ /* nsm = nsm_new(); */ /* */ /* nsm_set_open_callback( nsm, cb_nsm_open, 0 ); */ /* nsm_set_save_callback( nsm, cb_nsm_save, 0 ); */ /* */ /* if ( 0 == nsm_init( nsm, nsm_url ) ) */ /* { */ /* nsm_send_announce( nsm, "FOO", ":optional-gui:", argv[0] );*/ /* */ /* ********************************************************************** */ /* This will block for at most 100 sec and */ /* waiting for the NSM server open callback. */ /* DISCLAIMER: YOU MAY NOT NEED TO DO THAT. */ /* ********************************************************************** */ /* */ /* int timeout = 0; */ /* while ( wait_nsm ) */ /* { */ /* nsm_check_wait( nsm, 500 ); */ /* timeout += 1; */ /* if ( timeout > 200 ) */ /* exit ( 1 ); */ /* } */ /* */ /* ********************************************************************** */ /* This will check if the server support optional-gui */ /* and connect the callbacks when support is found. */ /* If you don't use the above blocking block */ /* this could be done in cb_nsm_open() as well. */ /* DISCLAIMER: YOU MAY NOT NEED TO DO THAT. */ /* ********************************************************************** */ /* */ /* if ( strstr( nsm_get_session_manager_features ( nsm ), */ /* ":optional-gui:" ) ) */ /* { */ /* nsm_set_show_callback( nsm, cb_nsm_show, 0 ); */ /* nsm_set_hide_callback( nsm, cb_nsm_hide, 0 ); */ /* } */ /* */ /* ********************************************************************** */ /* */ /* do_timeout_add( 200, poll_nsm, Null ); //Your own function */ /* } */ /* else */ /* { */ /* nsm_free( nsm ); */ /* nsm = 0; */ /* } */ /* } */ /* } */ /**************************************************************************/ #ifndef _NSM_H #define _NSM_H #define NSM_API_VERSION_MAJOR 1 #define NSM_API_VERSION_MINOR 0 #include <lo/lo.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> typedef void * nsm_client_t; typedef int (nsm_open_callback)( const char *name, const char *display_name, const char *client_id, char **out_msg, void *userdata ); typedef int (nsm_save_callback)( char **out_msg, void *userdata ); typedef void (nsm_show_gui_callback)( void *userdata ); typedef void (nsm_hide_gui_callback)( void *userdata ); typedef void (nsm_active_callback)( int b, void *userdata ); typedef void (nsm_session_is_loaded_callback)( void *userdata ); typedef int (nsm_broadcast_callback)( const char *, lo_message m, void *userdata ); #define _NSM() ((struct _nsm_client_t*)nsm) #define NSM_EXPORT __attribute__((unused)) static /* private parts */ struct _nsm_client_t { const char *nsm_url; lo_server _server; lo_server_thread _st; lo_address nsm_addr; int nsm_is_active; char *nsm_client_id; char *_session_manager_name; char *_session_manager_features; nsm_open_callback *open; void *open_userdata; nsm_save_callback *save; void *save_userdata; nsm_show_gui_callback *show; void *show_userdata; nsm_hide_gui_callback *hide; void *hide_userdata; nsm_active_callback *active; void *active_userdata; nsm_session_is_loaded_callback *session_is_loaded; void *session_is_loaded_userdata; nsm_broadcast_callback *broadcast; void *broadcast_userdata; }; enum { ERR_OK = 0, ERR_GENERAL = -1, ERR_INCOMPATIBLE_API = -2, ERR_BLACKLISTED = -3, ERR_LAUNCH_FAILED = -4, ERR_NO_SUCH_FILE = -5, ERR_NO_SESSION_OPEN = -6, ERR_UNSAVED_CHANGES = -7, ERR_NOT_NOW = -8 }; NSM_EXPORT int nsm_is_active ( nsm_client_t *nsm ) { return _NSM()->nsm_is_active; } NSM_EXPORT const char * nsm_get_session_manager_name ( nsm_client_t *nsm ) { return _NSM()->_session_manager_name; } NSM_EXPORT const char * nsm_get_session_manager_features ( nsm_client_t *nsm ) { return _NSM()->_session_manager_features; } NSM_EXPORT nsm_client_t * nsm_new ( void ) { struct _nsm_client_t *nsm = (struct _nsm_client_t*)malloc( sizeof( struct _nsm_client_t ) ); nsm->nsm_url = 0; nsm->nsm_is_active = 0; nsm->nsm_client_id = 0; nsm->_server = 0; nsm->_st = 0; nsm->nsm_addr = 0; nsm->_session_manager_name = 0; nsm->_session_manager_features = 0; nsm->open = 0; nsm->save = 0; nsm->show = 0; nsm->hide = 0; nsm->active = 0; nsm->session_is_loaded = 0; nsm->broadcast = 0; return (nsm_client_t *)nsm; } /*******************************************/ /* CLIENT TO SERVER INFORMATIONAL MESSAGES */ /*******************************************/ NSM_EXPORT void nsm_send_is_dirty ( nsm_client_t *nsm ) { if ( _NSM()->nsm_is_active ) lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/is_dirty", "" ); } NSM_EXPORT void nsm_send_is_clean ( nsm_client_t *nsm ) { if ( _NSM()->nsm_is_active ) lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/is_clean", "" ); } NSM_EXPORT void nsm_send_is_shown ( nsm_client_t *nsm ) { if ( _NSM()->nsm_is_active ) lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/gui_is_shown", "" ); } NSM_EXPORT void nsm_send_is_hidden ( nsm_client_t *nsm ) { if ( _NSM()->nsm_is_active ) lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" ); } NSM_EXPORT void nsm_send_progress ( nsm_client_t *nsm, float p ) { if ( _NSM()->nsm_is_active ) lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/progress", "f", p ); } NSM_EXPORT void nsm_send_message ( nsm_client_t *nsm, int priority, const char *msg ) { if ( _NSM()->nsm_is_active ) lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/message", "is", priority, msg ); } NSM_EXPORT void nsm_send_announce ( nsm_client_t *nsm, const char *app_name, const char *capabilities, const char *process_name ) { lo_address to = lo_address_new_from_url( _NSM()->nsm_url ); if ( ! to ) { fprintf( stderr, "NSM: Bad address!" ); return; } int pid = (int)getpid(); lo_send_from( to, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/server/announce", "sssiii", app_name, capabilities, process_name, NSM_API_VERSION_MAJOR, NSM_API_VERSION_MINOR, pid ); lo_address_free( to ); } NSM_EXPORT void nsm_send_broadcast ( nsm_client_t *nsm, lo_message msg ) { if ( _NSM()->nsm_is_active ) lo_send_message_from( _NSM()->nsm_addr, _NSM()->_server, "/nsm/server/broadcast", msg ); } NSM_EXPORT void nsm_check_wait ( nsm_client_t *nsm, int timeout ) { if ( lo_server_wait( _NSM()->_server, timeout ) ) while ( lo_server_recv_noblock( _NSM()->_server, 0 ) ) {} } NSM_EXPORT void nsm_check_nowait (nsm_client_t *nsm ) { nsm_check_wait( nsm, 0 ); } NSM_EXPORT void nsm_thread_start ( nsm_client_t *nsm ) { lo_server_thread_start( _NSM()->_st ); } NSM_EXPORT void nsm_thread_stop ( nsm_client_t *nsm ) { lo_server_thread_stop( _NSM()->_st ); } NSM_EXPORT void nsm_free ( nsm_client_t *nsm ) { if ( _NSM()->_st ) nsm_thread_stop( nsm ); if ( _NSM()->_st ) lo_server_thread_free( _NSM()->_st ); else lo_server_free( _NSM()->_server ); lo_address_free(_NSM()->nsm_addr); free(_NSM()->nsm_client_id); free(_NSM()->_session_manager_name); free(_NSM()->_session_manager_features); free( _NSM() ); } /*****************/ /* SET CALLBACKS */ /*****************/ NSM_EXPORT void nsm_set_open_callback( nsm_client_t *nsm, nsm_open_callback *open_callback, void *userdata ) { _NSM()->open = open_callback; _NSM()->open_userdata = userdata; } NSM_EXPORT void nsm_set_save_callback( nsm_client_t *nsm, nsm_save_callback *save_callback, void *userdata ) { _NSM()->save = save_callback; _NSM()->save_userdata = userdata; } NSM_EXPORT void nsm_set_show_callback( nsm_client_t *nsm, nsm_show_gui_callback *show_callback, void *userdata ) { _NSM()->show = show_callback; _NSM()->show_userdata = userdata; } NSM_EXPORT void nsm_set_hide_callback( nsm_client_t *nsm, nsm_hide_gui_callback *hide_callback, void *userdata ) { _NSM()->hide = hide_callback; _NSM()->hide_userdata = userdata; } NSM_EXPORT void nsm_set_active_callback( nsm_client_t *nsm, nsm_active_callback *active_callback, void *userdata ) { _NSM()->active = active_callback; _NSM()->active_userdata = userdata; } NSM_EXPORT void nsm_set_session_is_loaded_callback( nsm_client_t *nsm, nsm_session_is_loaded_callback *session_is_loaded_callback, void *userdata ) { _NSM()->session_is_loaded = session_is_loaded_callback; _NSM()->session_is_loaded_userdata = userdata; } NSM_EXPORT void nsm_set_broadcast_callback( nsm_client_t *nsm, nsm_broadcast_callback *broadcast_callback, void *userdata ) { _NSM()->broadcast = broadcast_callback; _NSM()->broadcast_userdata = userdata; } /****************/ /* OSC HANDLERS */ /****************/ #undef OSC_REPLY #undef OSC_REPLY_ERR #define OSC_REPLY( value ) lo_send_from( ((struct _nsm_client_t*)user_data)->nsm_addr, ((struct _nsm_client_t*)user_data)->_server, LO_TT_IMMEDIATE, "/reply", "ss", path, value ) #define OSC_REPLY_ERR( errcode, value ) lo_send_from( ((struct _nsm_client_t*)user_data)->nsm_addr, ((struct _nsm_client_t*)user_data)->_server, LO_TT_IMMEDIATE, "/error", "sis", path, errcode, value ) NSM_EXPORT int _nsm_osc_open ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { (void) types; (void) argc; (void) msg; char *out_msg = NULL; struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; nsm->nsm_client_id = strdup( &argv[2]->s ); if ( ! nsm->open ) return 0; int r = nsm->open( &argv[0]->s, &argv[1]->s, &argv[2]->s, &out_msg, nsm->open_userdata ); if ( r ) OSC_REPLY_ERR( r, ( out_msg ? out_msg : "") ); else OSC_REPLY( "OK" ); if ( out_msg ) free( out_msg ); return 0; } NSM_EXPORT int _nsm_osc_save ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { (void) types; (void) argv; (void) argc; (void) msg; char *out_msg = NULL; struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; if ( ! nsm->save ) return 0; int r = nsm->save(&out_msg, nsm->save_userdata ); if ( r ) OSC_REPLY_ERR( r, ( out_msg ? out_msg : "") ); else OSC_REPLY( "OK" ); if ( out_msg ) free( out_msg ); return 0; } NSM_EXPORT int _nsm_osc_announce_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { (void) path; (void) types; (void) argc; if ( strcmp( &argv[0]->s, "/nsm/server/announce" ) ) return -1; struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; fprintf( stderr, "NSM: Successfully registered. NSM says: %s", &argv[1]->s ); nsm->nsm_is_active = 1; nsm->_session_manager_name = strdup( &argv[2]->s ); nsm->_session_manager_features = strdup( &argv[3]->s ); nsm->nsm_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); if ( nsm->active ) nsm->active( nsm->nsm_is_active, nsm->active_userdata ); return 0; } NSM_EXPORT int _nsm_osc_error ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { (void) path; (void) types; (void) argc; (void) msg; if ( strcmp( &argv[0]->s, "/nsm/server/announce" ) ) return -1; struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; fprintf( stderr, "NSM: Failed to register with NSM server: %s", &argv[2]->s ); nsm->nsm_is_active = 0; if ( nsm->active ) nsm->active( nsm->nsm_is_active, nsm->active_userdata ); return 0; } NSM_EXPORT int _nsm_osc_session_is_loaded ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { (void) path; (void) types; (void) argv; (void) argc; (void) msg; struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; if ( ! nsm->session_is_loaded ) return 0; nsm->session_is_loaded( nsm->session_is_loaded_userdata ); return 0; } NSM_EXPORT int _nsm_osc_show ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; if ( ! nsm->show ) return 0; nsm->show( nsm->show_userdata ); return 0; } NSM_EXPORT int _nsm_osc_hide ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; if ( ! nsm->hide ) return 0; nsm->hide( nsm->hide_userdata ); return 0; } NSM_EXPORT int _nsm_osc_broadcast ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { (void) types; (void) argv; (void) argc; struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; if ( ! nsm->broadcast ) return 0; return nsm->broadcast( path, msg, nsm->broadcast_userdata ); } NSM_EXPORT int nsm_init ( nsm_client_t *nsm, const char *nsm_url ) { _NSM()->nsm_url = nsm_url; lo_address addr = lo_address_new_from_url( nsm_url ); int proto = lo_address_get_protocol( addr ); lo_address_free( addr ); _NSM()->_server = lo_server_new_with_proto( NULL, proto, NULL ); if ( ! _NSM()->_server ) return -1; lo_server_add_method( _NSM()->_server, "/error", "sis", _nsm_osc_error, _NSM() ); lo_server_add_method( _NSM()->_server, "/reply", "ssss", _nsm_osc_announce_reply, _NSM() ); lo_server_add_method( _NSM()->_server, "/nsm/client/open", "sss", _nsm_osc_open, _NSM() ); lo_server_add_method( _NSM()->_server, "/nsm/client/save", "", _nsm_osc_save, _NSM() ); lo_server_add_method( _NSM()->_server, "/nsm/client/session_is_loaded", "", _nsm_osc_session_is_loaded, _NSM() ); lo_server_add_method( _NSM()->_server, "/nsm/client/show_optional_gui", "", _nsm_osc_show, _NSM() ); lo_server_add_method( _NSM()->_server, "/nsm/client/hide_optional_gui", "", _nsm_osc_hide, _NSM() ); lo_server_add_method( _NSM()->_server, NULL, NULL, _nsm_osc_broadcast, _NSM() ); return 0; } NSM_EXPORT int nsm_init_thread ( nsm_client_t *nsm, const char *nsm_url ) { _NSM()->nsm_url = nsm_url; lo_address addr = lo_address_new_from_url( nsm_url ); int proto = lo_address_get_protocol( addr ); lo_address_free( addr ); _NSM()->_st = lo_server_thread_new_with_proto( NULL, proto, NULL ); _NSM()->_server = lo_server_thread_get_server( _NSM()->_st ); if ( ! _NSM()->_server ) return -1; lo_server_thread_add_method( _NSM()->_st, "/error", "sis", _nsm_osc_error, _NSM() ); lo_server_thread_add_method( _NSM()->_st, "/reply", "ssss", _nsm_osc_announce_reply, _NSM() ); lo_server_thread_add_method( _NSM()->_st, "/nsm/client/open", "sss", _nsm_osc_open, _NSM() ); lo_server_thread_add_method( _NSM()->_st, "/nsm/client/save", "", _nsm_osc_save, _NSM() ); lo_server_thread_add_method( _NSM()->_st, "/nsm/client/session_is_loaded", "", _nsm_osc_session_is_loaded, _NSM() ); lo_server_thread_add_method( _NSM()->_st, "/nsm/client/show_optional_gui", "", _nsm_osc_show, _NSM() ); lo_server_thread_add_method( _NSM()->_st, "/nsm/client/hide_optional_gui", "", _nsm_osc_hide, _NSM() ); lo_server_thread_add_method( _NSM()->_st, NULL, NULL, _nsm_osc_broadcast, _NSM() ); return 0; } #endif /* NSM_H */ 07070100000023000041ED0000000000000000000000036258A65C00000000000000000000000000000000000000000000003C00000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm07070100000024000081A40000000000000000000000016258A65C00000469000000000000000000000000000000000000004400000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/LICENSEMIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 07070100000025000081A40000000000000000000000016258A65C0000196A000000000000000000000000000000000000004600000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/README.md# pynsm NSM/New Session Manager client library in Python - No dependencies except Python3. PyNSMClient - A New Session Manager Client-Library in one file. Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. This library is licensed under the MIT license. Please check the file LICENSE for more information. This library has no version numbers, or releases. It is a "rolling" git. Copy it into your source and update only if you need new features. ## Short Instructions Before you start a word about control flow: NSM and any client, like this one, can be considered automation and remote-control of GUI operations, normally done by the user. That means this module needs to be included in your GUI code. The author personally creates the pynsm-object as early as possible in his PyQt Mainwindow class. Copy nsmclient.py to your own program and import and initialize it as early as possible (see below) Then add nsmClient.reactToMessage to your existing event loop. from nsmclient import NSMClient nsmClient = NSMClient(prettyName = niceTitle, #will raise an error and exit if this example is not run from NSM. saveCallback = saveCallbackFunction, openOrNewCallback = openOrNewCallbackFunction, supportsSaveStatus = False, # Change this to True if your program announces it's save status to NSM exitProgramCallback = exitCallbackFunction, hideGUICallback = None, #replace with your hiding function. You need to answer in your function with nsmClient.announceGuiVisibility(False) showGUICallback = None, #replace with your showing function. You need to answer in your function with nsmClient.announceGuiVisibility(True) broadcastCallback = None, #give a function that reacts to any broadcast by any other client. sessionIsLoadedCallback = None, #give a function that reacts to the one-time state when a session has fully loaded. loggingLevel = "info", #"info" for development or debugging, "error" for production. default is error. ) Don't forget to add nsmClient.reactToMessage to your event loop. * Each of the callbacks "save", "open/new" and "exit" receive three parameters: ourPath, sessionName, ourClientNameUnderNSM. * openOrNew gets called first. Init your jack client there with ourClientNameUnderNSM as name. * exitProgramCallback is the place to gracefully exit your program, including jack-client closing. * saveCallback gets called all the time. Use ourPath either as filename or as directory. * If you choose filename add an extension. * If you choose directory make sure that the filenames inside are static, no matter what project/session. The user must have no influence over file naming * broadcastCallback receives five parameters. The three standard: ourPath, sessionName, ourClientNameUnderNSM. And additionally messagePath and listOfArguments. MessagePath is entirely program specific, the number and type of arguments depend on the sender. * sessionIsLoadedCallback receives no parameters. It is ONLY send once, after the session is fully loaded. This is NOT the place to delay your announce. If you add your client to a running session (which must happen at some point) sessionLoaded will NOT get called. * Additional callbacks are: hideGUICallback and showGUICallback. These receive no parameters and need to answer with the function: nsmClient.announceGuiVisibility(bool). That means you can decline show or hide, dependending on the state of your program. The nsmClient object has methods and variables such as: * nsmClient.ourClientNameUnderNSM * Always use this name for your program * nsmClient.announceSaveStatus(False) * Announce your save status (dirty = False / clean = True), If your program sends those messages set supportsSaveStatus = True when intializing NSMClient with both hideGUICallback and showGUICallback * nsmClient.sessionName * nsmClient.ourOscUrl = osc.udp://{ip}:{port}/ Use this to broadcast your presence, to handshake communication between different programs * nsmClient.announceGuiVisibility(bool) * Announce if your GUI is visible (True) or not (False). Only works if you initialized NSMClient with both hideGUICallback and showGUICallback. Don't forget to send it once for your state after starting your program. * nsmcClient.changeLabel(prettyName) * Tell the GUI to append (prettyName) to our name. This is not saved by NSM but you need to send it yourself each startup. * nsmClient.serverSendSaveToSelf() * A clean solution to use the nsm save callbacks from within your program (Ctrl+S or File->Save). No need for redundant save mechanism. * nsmClient.serverSendExitToSelf() * A clean quit, without "client died unexpectedly". Use sys.exit() to exit your program in your nms-quit callback. * nsmClient.importResource(filepath) * Use this to load external resources, for example a sample file. It links the sample file into the session dir, according to the NSM rules, and returns the path of the linked file. * nsmClient.debugResetDataAndExit() * Deletes self.ourpath, which is the session save file or directory, recursively and exits the client. This is only meant for debugging and testing. ## Long Instructions * Read and start example.py, then read and understand nsmclient.py. It requires PyQt5 to execute and a brain to read. * There are several very minimal and basic clients in the directory `minimalClients/` * For your own program read and learn the NSM API: http://non.tuxfamily.org/nsm/API.html * The hard part about session management is not to use this lib or write your own but to make your program comply to the strict rules of session management. ## Additional Examples More examples can be found in `/minimalClients`. This mimics a minimal, but functional program. To actually run the programs and attach them to a running session execute the inluced file `source_me_with_port.bash <PORT>` where <PORT> is the NSM port the server printed out after starting. You can see that nsmclient.py is included in this dir again, to avoid redundancy only as symlink. In your real program this would be the real file. ## Sources and Influences * The New-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ * New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org * With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) 07070100000026000081ED0000000000000000000000016258A65C00001557000000000000000000000000000000000000005200000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/exampleBoilerplate.py#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from time import sleep # main event loop at the bottom of this file # nsm only from nsmclient import NSMClient # will raise an error and exit if this example is not run from NSM. ######################################################################## # General ######################################################################## niceTitle = "myNSMprogram" # This is the name of your program. This will display in NSM and can be used in your save file ######################################################################## # Prepare the NSM Client # This has to be done as the first thing because NSM needs to get the paths # and may quit if NSM environment var was not found. # # This is also where to set up your functions that react to messages from NSM, ######################################################################## # Here are some variables you can use: # ourPath = Full path to your NSM save file/directory with serialized NSM extension # ourClientNameUnderNSM = Your NSM save file/directory with serialized NSM extension and no path information # sessionName = The name of your session with no path information ######################################################################## def saveCallback(ourPath, sessionName, ourClientNameUnderNSM): # Put your code to save your config file in here print("saveCallback"); def openCallback(ourPath, sessionName, ourClientNameUnderNSM): # Put your code to open your config file in here print("openCallback"); def exitProgram(ourPath, sessionName, ourClientNameUnderNSM): """This function is a callback for NSM. We have a chance to close our clients and open connections here. If not nsmclient will just kill us no matter what """ print("exitProgram"); # Exit is done by NSM kill. def showGUICallback(): # Put your code that shows your GUI in here print("showGUICallback"); nsmClient.announceGuiVisibility(isVisible=True) # Inform NSM that the GUI is now visible. Put this at the end. def hideGUICallback(): # Put your code that hides your GUI in here print("hideGUICallback"); nsmClient.announceGuiVisibility(isVisible=False) # Inform NSM that the GUI is now hidden. Put this at the end. nsmClient = NSMClient(prettyName = niceTitle, saveCallback = saveCallback, openOrNewCallback = openCallback, showGUICallback = showGUICallback, # Comment this line out if your program does not have an optional GUI hideGUICallback = hideGUICallback, # Comment this line out if your program does not have an optional GUI supportsSaveStatus = False, # Change this to True if your program announces it's save status to NSM exitProgramCallback = exitProgram, loggingLevel = "info", # "info" for development or debugging, "error" for production. default is error. ) # If NSM did not start up properly the program quits here. ######################################################################## # If your project uses JACK, activate your client here # You can use jackClientName or create your own ######################################################################## jackClientName = nsmClient.ourClientNameUnderNSM ######################################################################## # Start main program loop. ######################################################################## # showGUICallback() # If you want your GUI to be shown by default, uncomment this line print("Entering main loop") while True: nsmClient.reactToMessage() # Make sure this exists somewhere in your main loop # nsmClient.announceSaveStatus(False) # Announce your save status (dirty = False / clean = True) sleep(0.05) 07070100000027000081ED0000000000000000000000016258A65C00002A8D000000000000000000000000000000000000004900000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/exampleQt.py#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import sys #for qt app args. from os import getpid #we use this as jack meta data from PyQt5 import QtWidgets, QtCore #jack only import ctypes from random import uniform #to generate samples between -1.0 and 1.0 for Jack. #nsm only from nsmclient import NSMClient ######################################################################## #Prepare the Qt Window ######################################################################## class Main(QtWidgets.QWidget): def __init__(self, qtApp): super().__init__() self.qtApp = qtApp self.layout = QtWidgets.QVBoxLayout() self.layout.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) self.setLayout(self.layout) niceTitle = "PyNSM v2 Example - JACK Noise" self.title = QtWidgets.QLabel("") self.saved = QtWidgets.QLabel("") self._value = QtWidgets.QSlider(orientation = 1) #horizontal self._value.setMinimum(0) self._value.setMaximum(100) self._value.setValue(50) #only visible for the first start. self.valueLabel = QtWidgets.QLabel("Noise Volume: " + str(self._value.value())) self._value.valueChanged.connect(lambda new: self.valueLabel.setText("Noise Volume: " + str(new))) self.layout.addWidget(self.title) self.layout.addWidget(self.saved) self.layout.addWidget(self._value) self.layout.addWidget(self.valueLabel) #Prepare the NSM Client #This has to be done as soon as possible because NSM provides paths and names for us. #and may quit if NSM environment var was not found. self.nsmClient = NSMClient(prettyName = niceTitle, #will raise an error and exit if this example is not run from NSM. supportsSaveStatus = True, saveCallback = self.saveCallback, openOrNewCallback = self.openOrNewCallback, exitProgramCallback = self.exitCallback, loggingLevel = "info", #"info" for development or debugging, "error" for production. default is error. ) #If NSM did not start up properly the program quits here with an error message from NSM. #No JACK client gets created, no Qt window can be seen. self.title.setText("<b>" + self.nsmClient.ourClientNameUnderNSM + "</b>") self.eventLoop = QtCore.QTimer() self.eventLoop.start(100) #10ms-20ms is smooth for "real time" feeling. 100ms is still ok. self.eventLoop.timeout.connect(self.nsmClient.reactToMessage) #self.show is called as the new/open callback. @property def value(self): return str(self._value.value()) @value.setter def value(self, new): new = int(new) self._value.setValue(new) def saveCallback(self, ourPath, sessionName, ourClientNameUnderNSM): #parameters are filled in by NSM. if self.value: with open(ourPath, "w") as f: #ourpath is taken as a filename here. We have this path name at our disposal and we know we only want one file. So we don't make a directory. This way we don't have to create a dir first. f.write(self.value) def openOrNewCallback(self, ourPath, sessionName, ourClientNameUnderNSM): #parameters are filled in by NSM. try: with open(ourPath, "r") as f: savedValue = f.read() #a string self.saved.setText("{}: {}".format(ourPath, savedValue)) self.value = savedValue #internal casting to int. Sets the slider. except FileNotFoundError: self.saved.setText("{}: No save file found. Normal for first start.".format(ourPath)) finally: self.show() def exitCallback(self, ourPath, sessionName, ourClientNameUnderNSM): """This function is a callback for NSM. We have a chance to close our clients and open connections here. If not nsmclient will just kill us no matter what """ cjack.jack_remove_properties(ctypesJackClient, ctypesJackUuid) #clean our metadata cjack.jack_client_close(ctypesJackClient) #omitting this introduces problems. in Jack1 this would mute all jack clients for several seconds. exit() #or get SIGKILLed through NSM def closeEvent(self, event): """Qt likes to quits on its own. For example when the window manager closes the main window. Ignore that request and instead send a roundtrip through NSM""" self.nsmClient.serverSendExitToSelf() event.ignore() #Prepare the window instance. Gets executed at the end of this file. qtApp = QtWidgets.QApplication(sys.argv) ourClient = Main(qtApp) ######################################################################## #Prepare the JACK Client #We need the client name from NSM first. ######################################################################## cjack = ctypes.cdll.LoadLibrary("libjack.so.0") clientName = ourClient.nsmClient.prettyName #the nsm client is in the qt instance here. But in your program it can be anywhere. options = 0 status = None class jack_client_t(ctypes.Structure): _fields_ = [] cjack.jack_client_open.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)] #the two ints are enum and pointer to enum. #http://jackaudio.org/files/docs/html/group__ClientFunctions.html#gab8b16ee616207532d0585d04a0bd1d60 cjack.jack_client_open.restype = ctypes.POINTER(jack_client_t) ctypesJackClient = cjack.jack_client_open(clientName.encode("ascii"), options, status) #Create one output port class jack_port_t(ctypes.Structure): _fields_ = [] JACK_DEFAULT_AUDIO_TYPE = "32 bit float mono audio".encode("ascii") #http://jackaudio.org/files/docs/html/types_8h.html JACK_PORT_IS_OUTPUT = 0x2 #http://jackaudio.org/files/docs/html/types_8h.html portname = "output".encode("ascii") cjack.jack_port_register.argtypes = [ctypes.POINTER(jack_client_t), ctypes.c_char_p, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_ulong] #http://jackaudio.org/files/docs/html/group__PortFunctions.html#ga3e21d145c3c82d273a889272f0e405e7 cjack.jack_port_register.restype = ctypes.POINTER(jack_port_t) outputPort = cjack.jack_port_register(ctypesJackClient, portname, JACK_DEFAULT_AUDIO_TYPE, JACK_PORT_IS_OUTPUT, 0) cjack.jack_client_close.argtypes = [ctypes.POINTER(jack_client_t),] #Create the callback #http://jackaudio.org/files/docs/html/group__ClientCallbacks.html#gafb5ec9fb4b736606d676c135fb97888b jack_nframes_t = ctypes.c_uint32 cjack.jack_port_get_buffer.argtypes = [ctypes.POINTER(jack_port_t), jack_nframes_t] cjack.jack_port_get_buffer.restype = ctypes.POINTER(ctypes.c_float) #this is only valid for audio, not for midi. C Jack has a pointer to void here. def pythonJackCallback(nframes, void): #types: jack_nframes_t (ctypes.c_uint32), pointer to void """http://jackaudio.org/files/docs/html/simple__client_8c.html#a01271cc6cf692278ae35d0062935d7ae""" out = cjack.jack_port_get_buffer(outputPort, nframes) #out should be a pointer to jack_default_audio_sample_t (float, ctypes.POINTER(ctypes.c_float)) #For each required sample for i in range(nframes): factor = ourClient._value.value() / 100 val = ctypes.c_float(round(uniform(-0.5, 0.5) * factor, 10)) out[i]= val return 0 # 0 on success, otherwise a non-zero error code, causing JACK to remove that client from the process() graph. JACK_CALLBACK_TYPE = ctypes.CFUNCTYPE(ctypes.c_int, jack_nframes_t, ctypes.c_void_p) #the first parameter is the return type, the following are input parameters callbackFunction = JACK_CALLBACK_TYPE(pythonJackCallback) cjack.jack_set_process_callback.argtypes = [ctypes.POINTER(jack_client_t), JACK_CALLBACK_TYPE, ctypes.c_void_p] cjack.jack_set_process_callback.restype = ctypes.c_uint32 #I think this is redundant since ctypes has int as default result type cjack.jack_set_process_callback(ctypesJackClient, callbackFunction, 0) #Ready. Activate the client. cjack.jack_activate(ctypesJackClient) #The Jack Processing functions gets called by jack in another thread. We just have to keep this program itself running. Qt does the job. #Jack Metadata - Inform the jack server about our program. Optional but has benefits when used with other programs that rely on metadata. #http://jackaudio.org/files/docs/html/group__Metadata.html jack_uuid_t = ctypes.c_uint64 cjack.jack_set_property.argtypes = [ctypes.POINTER(jack_client_t), jack_uuid_t, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)] #client(we), subject/uuid,key,value/data,type cjack.jack_remove_properties.argtypes = [ctypes.POINTER(jack_client_t), jack_uuid_t] #for cleaning up when the program stops. the jack server can do it in newer jack versions, but this is safer. cjack.jack_get_uuid_for_client_name.argtypes = [ctypes.POINTER(jack_client_t), ctypes.c_char_p] cjack.jack_get_uuid_for_client_name.restype = ctypes.c_char_p ourJackUuid = cjack.jack_get_uuid_for_client_name(ctypesJackClient, clientName.encode("ascii")) ourJackUuid = int(ourJackUuid.decode("UTF-8")) ctypesJackUuid = jack_uuid_t(ourJackUuid) cjack.jack_set_property(ctypesJackClient, ctypesJackUuid, ctypes.c_char_p(b"pid"), ctypes.c_char_p(str(getpid()).encode()), None) ################## #Start everything qtApp.exec_() 07070100000028000041ED0000000000000000000000026258A65C00000000000000000000000000000000000000000000004B00000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/minimalClients07070100000029000081ED0000000000000000000000016258A65C000010A4000000000000000000000000000000000000005300000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/minimalClients/base.py#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ #/nsm/server/announce #A hack to get this into Agordejo launcher discovery from nsmclient import NSMClient import sys from time import sleep import os from threading import Timer sys.path.append(os.getcwd()) class BaseClient(object): def saveCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM): print (__file__, "save") def openOrNewCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM): print (__file__,"open/new") def exitCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM): print (__file__, "quit") sys.exit() def broadcastCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM, messagePath, listOfArguments): print (__file__, "broadcast") def event(self, nsmClient): pass def __init__(self, name, delayedFunctions=[], eventFunction=None): """delayedFunctions are a (timer delay in seconds, function call) list of tuples. They will be executed once. If the function is a string instead it will be evaluated in the BaseClient context, providing self. Do not give a lambda! Give eventFunction for repeated execution.""" self.nsmClient = NSMClient(prettyName = name, #will raise an error and exit if this example is not run from NSM. saveCallback = self.saveCallbackFunction, openOrNewCallback = self.openOrNewCallbackFunction, supportsSaveStatus = False, # Change this to True if your program announces it's save status to NSM exitProgramCallback = self.exitCallbackFunction, broadcastCallback = self.broadcastCallbackFunction, hideGUICallback = None, #replace with your hiding function. You need to answer in your function with nsmClient.announceGuiVisibility(False) showGUICallback = None, #replace with your showing function. You need to answer in your function with nsmClient.announceGuiVisibility(True) loggingLevel = "info", #"info" for development or debugging, "error" for production. default is error. ) if eventFunction: self.event = eventFunction for delay, func in delayedFunctions: if type(func) is str: func = eval('lambda self=self: ' + func ) t = Timer(interval=delay, function=func, args=()) t.start() while True: self.nsmClient.reactToMessage() self.event(self.nsmClient) sleep(0.05) if __name__ == '__main__': """This is the most minimal nsm client in existence""" BaseClient(name="testclient_base") #this never returns an object. 0707010000002A000081ED0000000000000000000000016258A65C000008A4000000000000000000000000000000000000005800000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/minimalClients/broadcast.py#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ #/nsm/server/announce #A hack to get this into Agordejo launcher discovery import base from time import sleep def hello(nsmClient): nsmClient.broadcast("/index/counter", [hello.i]) hello.i += 1 sleep(1) hello.i = 0 class BroadcastClient(base.BaseClient): def broadcastCallbackFunction(self, ourPath, sessionName, ourClientNameUnderNSM, messagePath, listOfArguments): print (f"We are only an example client, but surely it would be valuable to react to {messagePath} for {listOfArguments}") if __name__ == '__main__': BroadcastClient(name="testclient_broadcast", eventFunction=hello) #this never returns an object. 0707010000002B000081ED0000000000000000000000016258A65C00000769000000000000000000000000000000000000005400000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/minimalClients/label.py#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ #/nsm/server/announce #A hack to get this into Agordejo launcher discovery import base if __name__ == '__main__': funcs = [ (1, 'self.nsmClient.changeLabel("Pretty Name")'), #(4, lambda: print ("four")), ] base.BaseClient(name="testclient_label", delayedFunctions=funcs) #this never returns an object. 0707010000002C0000A1FF00000000000000000000000162D27ACA0000000F000000000000000000000000000000000000005800000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/minimalClients/nsmclient.py../nsmclient.py0707010000002D000081A40000000000000000000000016258A65C000000F1000000000000000000000000000000000000006400000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/minimalClients/source_me_with_port.bash#!/bin/bash if [[ -z "$1" ]]; then echo "Give NSM OSC Port as only parameter. Afterwards you can run the executable pythons in this dir, directly, without ./" exit 1 fi export NSM_URL=osc.udp://0.0.0.0:$1/ export PATH=$(pwd):$PATH 0707010000002E000081A40000000000000000000000016258A65C00008589000000000000000000000000000000000000004900000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/nsmclient.py#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import logging; logger: logging.Logger #filled by init with client logger. import struct import socket from os import getenv, getpid, kill import os import os.path import shutil from uuid import uuid4 from sys import argv from signal import signal, SIGTERM, SIGINT, SIGKILL #react to exit signals to close the client gracefully. Or kill if the client fails to do so. from urllib.parse import urlparse class _IncomingMessage(object): """Representation of a parsed datagram representing an OSC message. An OSC message consists of an OSC Address Pattern followed by an OSC Type Tag String followed by zero or more OSC Arguments. """ def __init__(self, dgram): #NSM Broadcasts are bundles, but very simple ones. We only need to care about the single message it contains. #Therefore we can strip the bundle prefix and handle it as normal message. if b"#bundle" in dgram: bundlePrefix, singleMessage = dgram.split(b"/", maxsplit=1) dgram = b"/" + singleMessage # / eaten by split self.isBroadcast = True else: self.isBroadcast = False self.LENGTH = 4 #32 bit self._dgram = dgram self._parameters = [] self.parse_datagram() def get_int(self, dgram, start_index): """Get a 32-bit big-endian two's complement integer from the datagram. Args: dgram: A datagram packet. start_index: An index where the integer starts in the datagram. Returns: A tuple containing the integer and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: if len(dgram[start_index:]) < self.LENGTH: raise ValueError('Datagram is too short') return ( struct.unpack('>i', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def get_string(self, dgram, start_index): """Get a python string from the datagram, starting at pos start_index. We receive always the full string, but handle only the part from the start_index internally. In the end return the offset so it can be added to the index for the next parameter. Each subsequent call handles less of the same string, starting further to the right. According to the specifications, a string is: "A sequence of non-null ASCII characters followed by a null, followed by 0-3 additional null characters to make the total number of bits a multiple of 32". Args: dgram: A datagram packet. start_index: An index where the string starts in the datagram. Returns: A tuple containing the string and the new end index. Raises: ValueError if the datagram could not be parsed. """ #First test for empty string, which is nothing, followed by a terminating \x00 padded by three additional \x00. if dgram[start_index:].startswith(b"\x00\x00\x00\x00"): return "", start_index + 4 #Otherwise we have a non-empty string that must follow the rules of the docstring. offset = 0 try: while dgram[start_index + offset] != 0: offset += 1 if offset == 0: raise ValueError('OSC string cannot begin with a null byte: %s' % dgram[start_index:]) # Align to a byte word. if (offset) % self.LENGTH == 0: offset += self.LENGTH else: offset += (-offset % self.LENGTH) # Python slices do not raise an IndexError past the last index, # do it ourselves. if offset > len(dgram[start_index:]): raise ValueError('Datagram is too short') data_str = dgram[start_index:start_index + offset] return data_str.replace(b'\x00', b'').decode('utf-8'), start_index + offset except IndexError as ie: raise ValueError('Could not parse datagram %s' % ie) except TypeError as te: raise ValueError('Could not parse datagram %s' % te) def get_float(self, dgram, start_index): """Get a 32-bit big-endian IEEE 754 floating point number from the datagram. Args: dgram: A datagram packet. start_index: An index where the float starts in the datagram. Returns: A tuple containing the float and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: return (struct.unpack('>f', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def parse_datagram(self): try: self._address_regexp, index = self.get_string(self._dgram, 0) if not self._dgram[index:]: # No params is legit, just return now. return # Get the parameters types. type_tag, index = self.get_string(self._dgram, index) if type_tag.startswith(','): type_tag = type_tag[1:] # Parse each parameter given its type. for param in type_tag: if param == "i": # Integer. val, index = self.get_int(self._dgram, index) elif param == "f": # Float. val, index = self.get_float(self._dgram, index) elif param == "s": # String. val, index = self.get_string(self._dgram, index) else: logger.warning("Unhandled parameter type: {0}".format(param)) continue self._parameters.append(val) except ValueError as pe: #raise ValueError('Found incorrect datagram, ignoring it', pe) # Raising an error is not ignoring it! logger.warning("Found incorrect datagram, ignoring it. {}".format(pe)) @property def oscpath(self): """Returns the OSC address regular expression.""" return self._address_regexp @staticmethod def dgram_is_message(dgram): """Returns whether this datagram starts as an OSC message.""" return dgram.startswith(b'/') @property def size(self): """Returns the length of the datagram for this message.""" return len(self._dgram) @property def dgram(self): """Returns the datagram from which this message was built.""" return self._dgram @property def params(self): """Convenience method for list(self) to get the list of parameters.""" return list(self) def __iter__(self): """Returns an iterator over the parameters of this message.""" return iter(self._parameters) class _OutgoingMessage(object): def __init__(self, oscpath): self.LENGTH = 4 #32 bit self.oscpath = oscpath self._args = [] def write_string(self, val): dgram = val.encode('utf-8') diff = self.LENGTH - (len(dgram) % self.LENGTH) dgram += (b'\x00' * diff) return dgram def write_int(self, val): return struct.pack('>i', val) def write_float(self, val): return struct.pack('>f', val) def add_arg(self, argument): t = {str:"s", int:"i", float:"f"}[type(argument)] self._args.append((t, argument)) def build(self): dgram = b'' #OSC Path dgram += self.write_string(self.oscpath) if not self._args: dgram += self.write_string(',') return dgram # Write the parameters. arg_types = "".join([arg[0] for arg in self._args]) dgram += self.write_string(',' + arg_types) for arg_type, value in self._args: f = {"s":self.write_string, "i":self.write_int, "f":self.write_float}[arg_type] dgram += f(value) return dgram class NSMNotRunningError(Exception): """Error raised when environment variable $NSM_URL was not found.""" class NSMClient(object): """The representation of the host programs as NSM sees it. Technically consists of an udp server and a udp client. Does not run an event loop itself and depends on the host loop. E.g. a Qt timer or just a simple while True: sleep(0.1) in Python.""" def __init__(self, prettyName, supportsSaveStatus, saveCallback, openOrNewCallback, exitProgramCallback, hideGUICallback=None, showGUICallback=None, broadcastCallback=None, sessionIsLoadedCallback=None, loggingLevel = "info"): self.nsmOSCUrl = self.getNsmOSCUrl() #this fails and raises NSMNotRunningError if NSM is not available. Host programs can ignore it or exit their program. self.realClient = True self.cachedSaveStatus = None #save status checks for this. global logger logger = logging.getLogger(prettyName) logger.info("import") if loggingLevel == "info" or loggingLevel == 20: logging.basicConfig(level=logging.INFO) #development logger.info("Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!") #the NSM name is not ready yet so we just use the pretty name elif loggingLevel == "error" or loggingLevel == 40: logging.basicConfig(level=logging.ERROR) #production else: logging.warning("Unknown logging level: {}. Choose 'info' or 'error'".format(loggingLevel)) logging.basicConfig(level=logging.INFO) #development #given parameters, self.prettyName = prettyName #keep this consistent! Settle for one name. self.supportsSaveStatus = supportsSaveStatus self.saveCallback = saveCallback self.exitProgramCallback = exitProgramCallback self.openOrNewCallback = openOrNewCallback #The host needs to: Create a jack client with ourClientNameUnderNSM - Open the saved file and all its resources self.broadcastCallback = broadcastCallback self.hideGUICallback = hideGUICallback self.showGUICallback = showGUICallback self.sessionIsLoadedCallback = sessionIsLoadedCallback #Reactions get the raw _IncomingMessage OSC object #A client can add to reactions. self.reactions = { "/nsm/client/save" : self._saveCallback, "/nsm/client/show_optional_gui" : lambda msg: self.showGUICallback(), "/nsm/client/hide_optional_gui" : lambda msg: self.hideGUICallback(), "/nsm/client/session_is_loaded" : self._sessionIsLoadedCallback, #Hello source-code reader. You can add your own reactions here by nsmClient.reactions[oscpath]=func, where func gets the raw _IncomingMessage OSC object as argument. #broadcast is handled directly by the function because it has more parameters } #self.discardReactions = set(["/nsm/client/session_is_loaded"]) self.discardReactions = set() #Networking and Init self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp self.sock.bind(('', 0)) #pick a free port on localhost. ip, port = self.sock.getsockname() self.ourOscUrl = f"osc.udp://{ip}:{port}/" self.executableName = self.getExecutableName() #UNIX Signals. Used for quit. signal(SIGTERM, self.sigtermHandler) #NSM sends only SIGTERM. #TODO: really? pynsm version 1 handled sigkill as well. signal(SIGINT, self.sigtermHandler) #The following instance parameters are all set in announceOurselves self.serverFeatures = None self.sessionName = None self.ourPath = None self.ourClientNameUnderNSM = None self.ourClientId = None # the "file extension" of ourClientNameUnderNSM self.isVisible = None #set in announceGuiVisibility self.saveStatus = True # true is clean. false means we need saving. self.announceOurselves() assert self.serverFeatures, self.serverFeatures assert self.sessionName, self.sessionName assert self.ourPath, self.ourPath assert self.ourClientNameUnderNSM, self.ourClientNameUnderNSM self.sock.setblocking(False) #We have waited for tha handshake. Now switch blocking off because we expect sock.recvfrom to be empty in 99.99...% of the time so we shouldn't wait for the answer. #After this point the host must include self.reactToMessage in its event loop #We assume we are save at startup. self.announceSaveStatus(isClean = True) logger.info("NSMClient client init complete. Going into listening mode.") def reactToMessage(self): """This is the main loop message. It is added to the clients event loop.""" try: data, addr = self.sock.recvfrom(4096) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096. except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. return None msg = _IncomingMessage(data) if msg.oscpath in self.reactions: self.reactions[msg.oscpath](msg) elif msg.oscpath in self.discardReactions: pass elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/open", "Loaded."]: #NSM sends that all programs of the session were loaded. logger.info ("Got /reply Loaded from NSM Server") elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/save", "Saved."]: #NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually logger.info ("Got /reply Saved from NSM Server") elif msg.isBroadcast: if self.broadcastCallback: logger.info (f"Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}") self.broadcastCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM, msg.oscpath, msg.params) else: logger.info (f"No callback for broadcast! Got messagePath {msg.oscpath} and listOfArguments {msg.params}") elif msg.oscpath == "/error": logger.warning("Got /error from NSM Server. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) else: logger.warning("Reaction not implemented:. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) def send(self, path:str, listOfParameters:list, host=None, port=None): """Send any osc message. Defaults to nsmd URL. Will not wait for an answer but return None.""" if host and port: url = (host, port) else: url = self.nsmOSCUrl msg = _OutgoingMessage(path) for arg in listOfParameters: msg.add_arg(arg) #type is auto-determined by outgoing message self.sock.sendto(msg.build(), url) def getNsmOSCUrl(self): """Return and save the nsm osc url or raise an error""" nsmOSCUrl = getenv("NSM_URL") if not nsmOSCUrl: raise NSMNotRunningError("New-Session-Manager environment variable $NSM_URL not found.") else: #osc.udp://hostname:portnumber/ o = urlparse(nsmOSCUrl) #return o.hostname, o.port #this always make the hostname lowercase. usually it does not matter, but we got crash reports. Alternative: return o.netloc.split(":")[0], o.port def getExecutableName(self): """Finding the actual executable name can be a bit hard in Python. NSM wants the real starting point, even if it was a bash script. """ #TODO: I really don't know how to find out the name of the bash script fullPath = argv[0] assert os.path.dirname(fullPath) in os.environ["PATH"], (fullPath, os.path.dirname(fullPath), os.environ["PATH"]) #NSM requires the executable to be in the path. No excuses. This will never happen since the reference NSM server-GUI already checks for this. executableName = os.path.basename(fullPath) assert not "/" in executableName, executableName #see above. return executableName def announceOurselves(self): """Say hello to NSM and tell it we are ready to receive instructions /nsm/server/announce s:application_name s:capabilities s:executable_name i:api_version_major i:api_version_minor i:pid""" def buildClientFeaturesString(): #:dirty:switch:progress: result = [] if self.supportsSaveStatus: result.append("dirty") if self.hideGUICallback and self.showGUICallback: result.append("optional-gui") if result: return ":".join([""] + result + [""]) else: return "" logger.info("Sending our NSM-announce message") announce = _OutgoingMessage("/nsm/server/announce") announce.add_arg(self.prettyName) #s:application_name announce.add_arg(buildClientFeaturesString()) #s:capabilities announce.add_arg(self.executableName) #s:executable_name announce.add_arg(1) #i:api_version_major announce.add_arg(2) #i:api_version_minor announce.add_arg(int(getpid())) #i:pid hostname, port = self.nsmOSCUrl assert hostname, self.nsmOSCUrl assert port, self.nsmOSCUrl self.sock.sendto(announce.build(), self.nsmOSCUrl) #Wait for /reply (aka 'Howdy, what took you so long?) data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if msg.oscpath == "/error": originalMessage, errorCode, reason = msg.params logger.error("Code {}: {}".format(errorCode, reason)) quit() elif msg.oscpath == "/reply": nsmAnnouncePath, welcomeMessage, managerName, self.serverFeatures = msg.params assert nsmAnnouncePath == "/nsm/server/announce", nsmAnnouncePath logger.info("Got /reply " + welcomeMessage) #Wait for /nsm/client/open data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) assert msg.oscpath == "/nsm/client/open", msg.oscpath self.ourPath, self.sessionName, self.ourClientNameUnderNSM = msg.params self.ourClientId = os.path.splitext(self.ourClientNameUnderNSM)[1][1:] logger.info("Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath)) self.openOrNewCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #Host function to either load an existing session or create a new one. logger.info("Our client should be done loading or creating the file {}".format(self.ourPath)) replyToOpen = _OutgoingMessage("/reply") replyToOpen.add_arg("/nsm/client/open") replyToOpen.add_arg("{} is opened or created".format(self.prettyName)) self.sock.sendto(replyToOpen.build(), self.nsmOSCUrl) else: raise ValueError("Unexpected message path after announce: {}".format((msg.oscpath, msg.params))) def announceGuiVisibility(self, isVisible): message = "/nsm/client/gui_is_shown" if isVisible else "/nsm/client/gui_is_hidden" self.isVisible = isVisible guiVisibility = _OutgoingMessage(message) logger.info("Telling NSM that our clients switched GUI visibility to: {}".format(message)) self.sock.sendto(guiVisibility.build(), self.nsmOSCUrl) def announceSaveStatus(self, isClean): """Only send to the NSM Server if there was really a change""" if not self.supportsSaveStatus: return if not isClean == self.cachedSaveStatus: message = "/nsm/client/is_clean" if isClean else "/nsm/client/is_dirty" self.cachedSaveStatus = isClean saveStatus = _OutgoingMessage(message) logger.info("Telling NSM that our clients save state is now: {}".format(message)) self.sock.sendto(saveStatus.build(), self.nsmOSCUrl) def _saveCallback(self, msg): logger.info("Telling our client to save as {}".format(self.ourPath)) self.saveCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) replyToSave = _OutgoingMessage("/reply") replyToSave.add_arg("/nsm/client/save") replyToSave.add_arg("{} saved".format(self.prettyName)) self.sock.sendto(replyToSave.build(), self.nsmOSCUrl) #it is assumed that after saving the state is clear self.announceSaveStatus(isClean = True) def _sessionIsLoadedCallback(self, msg): if self.sessionIsLoadedCallback: logger.info("Received 'Session is Loaded'. Our client supports it. Forwarding message...") self.sessionIsLoadedCallback() else: logger.info("Received 'Session is Loaded'. Our client does not support it, which is the default. Discarding message...") def sigtermHandler(self, signal, frame): """Wait for the user to quit the program The user function does not need to exit itself. Just shutdown audio engines etc. It is possible, that the client does not implement quit properly. In that case NSM protocol demands that we quit anyway. No excuses. Achtung GDB! If you run your program with gdb --args python foo.py the Python signal handler will not work. This has nothing to do with this library. """ logger.info("Telling our client to quit.") self.exitProgramCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #There is a chance that exitProgramCallback will hang and the program won't quit. However, this is broken design and bad programming. We COULD place a timeout here and just kill after 10s or so, but that would make quitting our responsibility and fixing a broken thing. #If we reach this point we have reached the point of no return. Say goodbye. logger.warning("Client did not quit on its own. Sending SIGKILL.") kill(getpid(), SIGKILL) logger.error("SIGKILL did nothing. Do it manually.") def debugResetDataAndExit(self): """This is solely meant for debugging and testing. The user way of action should be to remove the client from the session and add a new instance, which will get a different NSM-ID. Afterwards we perform a clean exit.""" logger.warning("debugResetDataAndExit will now delete {} and then request an exit.".format(self.ourPath)) if os.path.exists(self.ourPath): if os.path.isfile(self.ourPath): try: os.remove(self.ourPath) except Exception as e: logger.info(e) elif os.path.isdir(self.ourPath): try: shutil.rmtree(self.ourPath) except Exception as e: logger.info(e) else: logger.info("{} does not exist.".format(self.ourPath)) self.serverSendExitToSelf() def serverSendExitToSelf(self): """If you want a very strict client you can block any non-NSM quit-attempts, like ignoring a qt closeEvent, and instead send the NSM Server a request to close this client. This method is a shortcut to do just that. """ logger.info("Sending SIGTERM to ourselves to trigger the exit callback.") #if "server-control" in self.serverFeatures: # message = _OutgoingMessage("/nsm/server/stop") # message.add_arg("{}".format(self.ourClientId)) # self.sock.sendto(message.build(), self.nsmOSCUrl) #else: kill(getpid(), SIGTERM) #this calls the exit callback def serverSendSaveToSelf(self): """Some clients want to offer a manual Save function, mostly for psychological reasons. We offer a clean solution in calling this function which will trigger a round trip over the NSM server so our client thinks it received a Save instruction. This leads to a clean state with a good saveStatus and no required extra functionality in the client.""" logger.info("instructing the NSM-Server to send Save to ourselves.") if "server-control" in self.serverFeatures: #message = _OutgoingMessage("/nsm/server/save") # "Save All" Command. message = _OutgoingMessage("/nsm/gui/client/save") message.add_arg("{}".format(self.ourClientId)) self.sock.sendto(message.build(), self.nsmOSCUrl) else: logger.warning("...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures)) def changeLabel(self, label:str): """This function is implemented because it is provided by NSM. However, it does not much. The message gets received but is not saved. The official NSM GUI uses it but then does not save it. We would have to send it every startup ourselves. This is fine for us as clients, but you need to provide a GUI field to enter that label.""" logger.info("Telling the NSM-Server that our label is now " + label) message = _OutgoingMessage("/nsm/client/label") message.add_arg(label) #s:label self.sock.sendto(message.build(), self.nsmOSCUrl) def broadcast(self, path:str, arguments:list): """/nsm/server/broadcast s:path [arguments...] We, as sender, will not receive the broadcast back. Broadcasts starting with /nsm are not allowed and will get discarded by the server """ if path.startswith("/nsm"): logger.warning("Attempted broadbast starting with /nsm. Not allwoed") else: logger.info("Sending broadcast " + path + repr(arguments)) message = _OutgoingMessage("/nsm/server/broadcast") message.add_arg(path) for arg in arguments: message.add_arg(arg) #type autodetect self.sock.sendto(message.build(), self.nsmOSCUrl) def importResource(self, filePath): """aka. import into session ATTENTION! You will still receive an absolute path from this function. You need to make sure yourself that this path will not be saved in your save file, but rather use a place- holder that gets replaced by the actual session path each time. A good point is after serialisation. search&replace for the session prefix ("ourPath") and replace it with a tag e.g. <sessionDirectory>. The opposite during load. Only such a behaviour will make your session portable. Do not use the following pattern: An alternative that comes to mind is to only work with relative paths and force your programs workdir to the session directory. Better work with absolute paths internally . Symlinks given path into session dir and returns the linked path relative to the ourPath. It can handles single files as well as whole directories. if filePath is already a symlink we do not follow it. os.path.realpath or os.readlink will not be used. Multilayer links may indicate a users ordering system that depends on abstractions. e.g. with mounted drives under different names which get symlinked to a reliable path. Basically do not question the type of our input filePath. tar with the follow symlink option has os.path.realpath behaviour and therefore is able to follow multiple levels of links anyway. A hardlink does not count as a link and will be detected and treated as real file. Cleaning up a session directory is either responsibility of the user or of our client program. We do not provide any means to unlink or delete files from the session directory. """ #Even if the project was not saved yet now it is time to make our directory in the NSM dir. if not os.path.exists(self.ourPath): os.makedirs(self.ourPath) filePath = os.path.abspath(filePath) #includes normalisation if not os.path.exists(self.ourPath):raise FileNotFoundError(self.ourPath) if not os.path.isdir(self.ourPath): raise NotADirectoryError(self.ourPath) if not os.access(self.ourPath, os.W_OK): raise PermissionError("not writable", self.ourPath) if not os.path.exists(filePath):raise FileNotFoundError(filePath) if os.path.isdir(filePath): raise IsADirectoryError(filePath) if not os.access(filePath, os.R_OK): raise PermissionError("not readable", filePath) filePathInOurSession = os.path.commonprefix([filePath, self.ourPath]) == self.ourPath linkedPath = os.path.join(self.ourPath, os.path.basename(filePath)) linkedPathAlreadyExists = os.path.exists(linkedPath) if not os.access(os.path.dirname(linkedPath), os.W_OK): raise PermissionError("not writable", os.path.dirname(linkedPath)) if filePathInOurSession: #loadResource from our session dir. Portable session, manually copied beforehand or just loading a link again. linkedPath = filePath #we could return here, but we continue to get the tests below. logger.info(f"tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ") elif linkedPathAlreadyExists and os.readlink(linkedPath) == filePath: #the imported file already exists as link in our session dir. We do not link it again but simply report the existing link. #We only check for the first target of the existing link and do not follow it through to a real file. #This way all user abstractions and file structures will be honored. linkedPath = linkedPath logger.info(f"tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath} ") elif linkedPathAlreadyExists: #A new file shall be imported but it would create a linked name which already exists in our session dir. #Because we already checked for a new link to the same file above this means actually linking a different file so we need to differentiate with a unique name firstpart, extension = os.path.splitext(linkedPath) uniqueLinkedPath = firstpart + "." + uuid4().hex + extension assert not os.path.exists(uniqueLinkedPath) os.symlink(filePath, uniqueLinkedPath) logger.info(self.ourClientNameUnderNSM + f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.") linkedPath = uniqueLinkedPath else: #this is the "normal" case. External resources will be linked. assert not os.path.exists(linkedPath) os.symlink(filePath, linkedPath) logger.info(f"imported external resource {filePath} as link {linkedPath}") assert os.path.exists(linkedPath), linkedPath return linkedPath class NullClient(object): """Use this as a drop-in replacement if your program has a mode without NSM but you don't want to change the code itself. This was originally written for programs that have a core-engine and normal mode of operations is a GUI with NSM but they also support commandline-scripts and batch processing. For these you don't want NSM.""" def __init__(self, *args, **kwargs): self.realClient = False self.ourClientNameUnderNSM = "NSM Null Client" def announceSaveStatus(self, *args): pass def announceGuiVisibility(self, *args): pass def reactToMessage(self): pass def importResource(self): return "" def serverSendExitToSelf(self): quit() 0707010000002F000081ED0000000000000000000000016258A65C000011A0000000000000000000000000000000000000005300000000new-session-manager-1.6.0+git.20220415.0f6719c/extras/pynsm/test-importResource.py#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles <male@tuxfamily.org>: http://non.tuxfamily.org/nsm/ New Session Manager by Nils Hilbricht et al https://new-session-manager.jackaudio.org With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright (c) since 2014: Laborejo Software Suite <info@laborejo.org>, All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ #Test file for the resource import function in nsmclient.py if __name__ == "__main__": from nsmclient import NSMClient #We do not start nsmclient, just the the function with temporary files importResource = NSMClient.importResource import os, os.path import logging logging.basicConfig(level=logging.INFO) from inspect import currentframe def get_linenumber(): cf = currentframe() return cf.f_back.f_lineno from tempfile import mkdtemp class self(object): ourPath = mkdtemp() ourClientNameUnderNSM = "Loader Test" assert os.path.isdir(self.ourPath) #First a meta test to see if our system is working: assert os.path.exists("/etc/hostname") try: result = importResource(self, "/etc/hostname") #should not fail! except FileNotFoundError: pass else: print (f"Meta Test System works as of line {get_linenumber()}") print ("""You should not see any "Test Error" messages""") print ("Working in", self.ourPath) print (f"Removing {result} for a clean test environment") os.remove(result) print() #Real tests try: importResource(self, "/floot/nonexistent") #should fail except FileNotFoundError: pass else: print (f"Test Error in line {get_linenumber()}") try: importResource(self, "////floot//nonexistent/") #should fail except FileNotFoundError: pass else: print (f"Test Error in line {get_linenumber()}") try: importResource(self, "/etc/shadow") #reading not possible except PermissionError: pass else: print (f"Test Error in line {get_linenumber()}") assert os.path.exists("/etc/hostname") try: org = self.ourPath self.ourPath = "/" #writing not possible importResource(self, "/etc/hostname") except PermissionError: self.ourPath = org else: print (f"Test Error in line {get_linenumber()}") from tempfile import NamedTemporaryFile tmpf = NamedTemporaryFile() assert os.path.exists("/etc/hostname") try: org = self.ourPath self.ourPath = tmpf.name #writable, but not a dir importResource(self, "/etc/hostname") except NotADirectoryError: self.ourPath = org else: print (f"Test Error in line {get_linenumber()}") #Test the real purpose result = importResource(self, "/etc/hostname") print ("imported to", result) #Test what happens if we try to import already imported resource again result = importResource(self, result) print ("imported to", result) #Test what happens if we try to import a resource that would result in a name collision result = importResource(self, "/etc/hostname") print ("imported to", result) #Count the number of resulting files. assert len(os.listdir(self.ourPath)) == 2 07070100000030000081A40000000000000000000000016258A65C000012E1000000000000000000000000000000000000003B00000000new-session-manager-1.6.0+git.20220415.0f6719c/meson.build############################################################################## # Copyright (C) 2020- Nils Hilbricht # # This file is part of New-Session-Manager # # New-Session-Manager is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # New-Session-Manager is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>. ############################################################################## #Please keep the version number in a separate line so we can automatically update it. #Also keep it at the beginning of the line and leave spaces around the colon. #It's source is in src/nsmd.cpp #DEFINE VERSION_STRING project( 'new-session-manager', 'c', 'cpp', version : '1.6.0', license : 'GPLv3', ) ############## #Dependencies ############## liblodep = dependency('liblo') #and not 'lo' threaddep = dependency('threads') jackdep = dependency('jack', required: get_option('jackpatch')) #and not 'libjack' cc = meson.get_compiler('c') fltkdep = cc.find_library('fltk', required: get_option('nsm-legacy-gui') or get_option('nsm-proxy')) fltkimagesdep = cc.find_library('fltk_images', required: get_option('nsm-legacy-gui')) fluid = find_program('fluid', required: get_option('nsm-proxy')) ############## #Build Targets ############## executable('nsmd', sources: ['src/nsmd.cpp', 'src/debug.cpp', 'src/Endpoint.cpp', 'src/file.cpp', 'src/Thread.cpp'], dependencies: [liblodep, threaddep], install: true, ) install_man(['docs/src/nsmd.1']) install_data('docs/index.html', install_dir : get_option('datadir') / 'doc/new-session-manager') install_data('docs/api/index.html', install_dir : get_option('datadir') / 'doc/new-session-manager/api') install_data('CHANGELOG', install_dir : get_option('datadir') / 'doc/new-session-manager') install_data('README.md', install_dir : get_option('datadir') / 'doc/new-session-manager') #For options see meson_options.txt #All get_options are default=true if get_option('jackpatch') executable('jackpatch', 'src/jackpatch.c', dependencies: [liblodep, jackdep], install: true, ) install_data('src/jackpatch.svg', install_dir : get_option('datadir') / 'icons/hicolor/scalable/apps') install_data('src/org.jackaudio.jackpatch.desktop', install_dir : get_option('datadir') / 'applications') install_man(['docs/src/jackpatch.1', ]) endif if get_option('nsm-proxy') NSM_Proxy_UI_cpp = custom_target( 'NSM_Proxy_UI.cpp', output : 'NSM_Proxy_UI.C', input : 'src/NSM_Proxy_UI.fl', command : [fluid, '-c', '-o', '@OUTPUT@', '@INPUT@'], ) NSM_Proxy_UI_h = custom_target( 'NSM_Proxy_UI.h', output : 'NSM_Proxy_UI.H', input : 'src/NSM_Proxy_UI.fl', command : [fluid, '-c', '-h', '@OUTPUT@', '@INPUT@'], ) executable('nsm-proxy', sources: ['src/nsm-proxy.cpp', 'src/debug.cpp'], dependencies: [liblodep, threaddep], install: true, ) executable('nsm-proxy-gui', sources: ['src/nsm-proxy-gui.cpp', [NSM_Proxy_UI_cpp, NSM_Proxy_UI_h]], dependencies: [fltkdep, liblodep, threaddep], install: true, ) install_data('src/nsm-proxy.svg', install_dir : get_option('datadir') / 'icons/hicolor/scalable/apps') install_data('src/org.jackaudio.nsm-proxy.desktop', install_dir : get_option('datadir') / 'applications') install_man(['docs/src/nsm-proxy.1', 'docs/src/nsm-proxy-gui.1']) endif if get_option('nsm-legacy-gui') executable('nsm-legacy-gui', sources: ['src/nsm-legacy-gui.cpp', 'src/debug.cpp', 'src/Endpoint.cpp', 'src/Thread.cpp', 'src/FL/Fl_Scalepack.C'], dependencies: [fltkimagesdep, fltkdep, liblodep, threaddep], install: true, ) install_data('src/org.jackaudio.nsm-legacy-gui.desktop', install_dir : get_option('datadir') / 'applications') install_data('src/nsm-legacy-gui.svg', install_dir : get_option('datadir') / 'icons/hicolor/scalable/apps') install_man(['docs/src/nsm-legacy-gui.1', 'docs/src/non-session-manager.1']) #Symlinking is a one-way operation and can't be uninstalled, we rely on distribution packages for that meson.add_install_script('sh', '-c', 'ln -sf nsm-legacy-gui ${DESTDIR}@0@/@1@/non-session-manager'.format( get_option('prefix'), get_option('bindir'))) endif 07070100000031000081A40000000000000000000000016258A65C00000146000000000000000000000000000000000000004100000000new-session-manager-1.6.0+git.20220415.0f6719c/meson_options.txtoption('nsm-legacy-gui', type : 'boolean', value : true, description : 'Build nsm-legacy-gui, symlinked as non-session-manager') option('jackpatch', type : 'boolean', value : true, description : 'Build jackpatch client') option('nsm-proxy', type : 'boolean', value : true, description : 'Build nsm-proxy client with GUI.') 07070100000032000041ED0000000000000000000000036258A65C00000000000000000000000000000000000000000000003300000000new-session-manager-1.6.0+git.20220415.0f6719c/src07070100000033000081A40000000000000000000000016258A65C00009A43000000000000000000000000000000000000004000000000new-session-manager-1.6.0+git.20220415.0f6719c/src/Endpoint.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #include <lo/lo.h> #include "debug.h" #include <stdlib.h> #include <stdio.h> #include <string.h> #include <assert.h> #include "Endpoint.hpp" #include "Thread.hpp" #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wunused-result" namespace OSC { /**********/ /* Method */ /**********/ Method::Method ( ) { _path = _typespec = _documentation = 0; } Method::~Method ( ) { if ( _path ) free( _path ); if ( _typespec ) free( _typespec ); if ( _documentation ) free( _documentation ); } /**********/ /* Signal */ /**********/ Signal::Signal ( const char *path, Direction dir ) { _direction = dir; _path = NULL; if ( path ) _path = strdup( path ); _value = 0.0f; _endpoint = NULL; _peer = NULL; _documentation = 0; _user_data = 0; _connection_state_callback = 0; _connection_state_userdata = 0; } Signal::~Signal ( ) { if ( _endpoint ) { _endpoint->del_signal( this ); } if ( _path ) free( _path ); _path = NULL; _endpoint = NULL; } void Signal::rename ( const char *path ) { char *new_path; asprintf( &new_path, "%s%s", _endpoint->name() ? _endpoint->name() : "", path ); MESSAGE( "Renaming signal %s to %s", this->path(), new_path ); /* if ( _direction == Signal::Input ) */ /* { */ lo_server_del_method( _endpoint->_server, _path, NULL ); lo_server_add_method( _endpoint->_server, new_path, NULL, _endpoint->osc_sig_handler, this ); /* } */ for ( std::list<Peer*>::iterator i = _endpoint->_peers.begin(); i != _endpoint->_peers.end(); ++i ) { _endpoint->send( (*i)->addr, "/signal/renamed", _path, new_path ); } _endpoint->rename_translation_destination( _path, new_path ); free( _path ); _path = new_path; } void Signal::value ( float f ) { if ( f == _value ) return; _value = f; if ( direction() == Output ) { for ( std::list<Peer*>::iterator i = _endpoint->_peers.begin(); i != _endpoint->_peers.end(); ++i ) { _endpoint->send( (*i)->addr, path(), f ); } // free(s); } /* else if ( direction() == Input ) */ /* { */ /* MESSAGE( "Sending value feedback for signal %s...", path() ); */ /* for ( std::list<Signal*>::iterator i = _incoming.begin(); */ /* i != _incoming.end(); */ /* ++i ) */ /* { */ /* MESSAGE( "Sending value feedback to %s %s %f", lo_address_get_url( (*i)->_peer->addr), (*i)->path() , f); */ /* _endpoint->send( (*i)->_peer->addr, */ /* (*i)->path(), */ /* f ); */ /* } */ /* } */ } /* char * */ /* Signal::get_output_connection_peer_name_and_path ( int n ) */ /* { */ /* Signal *t = NULL; */ /* int j = 0; */ /* for ( std::list<Signal*>::const_iterator i = _outgoing.begin(); */ /* i != _outgoing.end(); */ /* ++i, ++j ) */ /* { */ /* if ( j == n ) */ /* { */ /* t = *i; */ /* break; */ /* } */ /* } */ /* if ( t ) */ /* { */ /* char *r; */ /* asprintf( &r, "%s%s", t->_peer->name, t->path() ); */ /* return r; */ /* } */ /* else */ /* return NULL; */ /* } */ /* */ void Endpoint::error_handler(int num, const char *msg, const char *path) { WARNING( "LibLO server error %d in path %s: %s\n", num, path, msg); } Endpoint::Endpoint ( ) { _learning_path = NULL; _peer_signal_notification_callback = 0; _peer_signal_notification_userdata = 0; _peer_scan_complete_callback = 0; _peer_scan_complete_userdata = 0; _server = 0; _name = 0; owner = 0; } int Endpoint::init ( int proto, const char *port ) { MESSAGE( "Creating OSC server" ); _server = lo_server_new_with_proto( port, proto, error_handler ); char *url = lo_server_get_url( _server ); _addr = lo_address_new_from_url( url ); free( url ); if ( ! _server ) { WARNING( "Error creating OSC server" ); return -1; } add_method( "/signal/hello", "ss", &Endpoint::osc_sig_hello, this, "" ); add_method( "/signal/connect", "ss", &Endpoint::osc_sig_connect, this, "" ); add_method( "/signal/disconnect", "ss", &Endpoint::osc_sig_disconnect, this, "" ); add_method( "/signal/renamed", "ss", &Endpoint::osc_sig_renamed, this, "" ); add_method( "/signal/removed", "s", &Endpoint::osc_sig_removed, this, "" ); add_method( "/signal/created", "ssfff", &Endpoint::osc_sig_created, this, "" ); add_method( "/signal/list", NULL, &Endpoint::osc_signal_lister, this, "" ); add_method( "/reply", NULL, &Endpoint::osc_reply, this, "" ); add_method( NULL, NULL, &Endpoint::osc_generic, this, "" ); return 0; } Endpoint::~Endpoint ( ) { // lo_server_thread_free( _st ); for ( std::list<Method*>::iterator i = _methods.begin(); i != _methods.end(); i++ ) delete(*i); _methods.clear(); if ( _server ) { lo_server_free( _server ); _server = 0; } lo_address_free( _addr ); _addr = 0; } OSC::Signal * Endpoint::find_target_by_peer_address ( std::list<Signal*> *l, lo_address addr ) { for ( std::list<Signal*>::iterator i = l->begin(); i != l->end(); ++i ) { if ( address_matches( addr, (*i)->_peer->addr ) ) { return *i; } } return NULL; } OSC::Signal * Endpoint::find_peer_signal_by_path ( Peer *p, const char *path ) { for ( std::list<Signal*>::iterator i = p->_signals.begin(); i != p->_signals.end(); ++i ) { if ( !strcmp( (*i)->path(), path ) ) return *i; } return NULL; } OSC::Signal * Endpoint::find_signal_by_path ( const char *path ) { for ( std::list<Signal*>::iterator i = _signals.begin(); i != _signals.end(); ++i ) { if ( !strcmp( (*i)->path(), path ) ) return *i; } return NULL; } void Endpoint::hello ( const char *url ) { assert( name() ); lo_address addr = lo_address_new_from_url ( url ); char *our_url = this->url(); send( addr, "/signal/hello", name(), our_url ); free( our_url ); lo_address_free( addr ); } void Endpoint::handle_hello ( const char *peer_name, const char *peer_url ) { MESSAGE( "Got hello from %s", peer_name ); Peer *p = find_peer_by_name( peer_name ); if ( ! p ) { scan_peer( peer_name, peer_url ); } else { /* maybe the peer has a new URL */ /* update address */ lo_address addr = lo_address_new_from_url( peer_url ); if ( address_matches( addr, p->addr ) ) { free( addr ); return; } if ( p->addr ) free( p->addr ); p->addr = addr; /* scan it while we're at it */ p->_scanning = true; MESSAGE( "Scanning peer %s", peer_name ); send( p->addr, "/signal/list" ); } if ( name() ) { hello( peer_url ); } else { MESSAGE( "Not sending hello because we don't have a name yet!" ); } } int Endpoint::osc_sig_hello ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { Endpoint *ep = (Endpoint*)user_data; const char *peer_name = &argv[0]->s; const char *peer_url = &argv[1]->s; ep->handle_hello( peer_name, peer_url ); return 0; } int Endpoint::osc_sig_disconnect ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { const char *their_name = &argv[0]->s; const char *our_name = &argv[1]->s; Endpoint *ep = (Endpoint*)user_data; Signal *s = ep->find_signal_by_path( our_name ); if ( ! s ) return 0; if ( s->_direction == Signal::Input ) { MESSAGE( "Peer %s has disconnected from signal %s", our_name, their_name ); ep->del_translation( their_name ); if ( s->_connection_state_callback ) s->_connection_state_callback( s, s->_connection_state_userdata ); return 0; } return 0; } int Endpoint::osc_sig_connect ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { const char *src_path = &argv[0]->s; const char *dst_path = &argv[1]->s; Endpoint *ep = (Endpoint*)user_data; Signal *dst_s = ep->find_signal_by_path( dst_path ); if ( ! dst_s ) { WARNING( "Unknown destination signal in connection attempt: \"%s\"", dst_path ); return 0; } if ( dst_s->_endpoint != ep ) { WARNING( "Got connection request for a destination signal we don't own" ); return 0; } MESSAGE( "Has requested signal connection %s |> %s", src_path, dst_s->path() ); ep->add_translation( src_path, dst_s->path() ); /* if ( dst_s->_connection_state_callback ) */ /* dst_s->_connection_state_callback( dst_s, dst_s->_connection_state_userdata ); */ return 0; } int Endpoint::osc_sig_removed ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { const char *name = &argv[0]->s; Endpoint *ep = (Endpoint*)user_data; Peer *p = ep->find_peer_by_address( lo_message_get_source( msg ) ); if ( ! p ) { WARNING( "Got signal removed notification from unknown peer." ); return 0; } Signal *o = ep->find_peer_signal_by_path( p, name ); if ( ! o ) { WARNING( "Unknown signal: %s", name ); return 0; } MESSAGE( "Signal %s:%s was removed", o->_peer->name, o->path() ); if ( ep->_peer_signal_notification_callback ) ep->_peer_signal_notification_callback( o, Signal::Removed, ep->_peer_signal_notification_userdata ); p->_signals.remove(o); delete o; return 0; } int Endpoint::osc_sig_created ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { Endpoint *ep = (Endpoint*)user_data; const char *name = &argv[0]->s; const char *direction = &argv[1]->s; const float min = argv[2]->f; const float max = argv[3]->f; const float default_value = argv[4]->f; Peer *p = ep->find_peer_by_address( lo_message_get_source( msg ) ); if ( ! p ) { WARNING( "Got signal creation notification from unknown peer" ); return 0; } Signal::Direction dir = Signal::Input; if ( !strcmp( direction, "in" ) ) dir = Signal::Input; else if ( !strcmp( direction, "out" ) ) dir = Signal::Output; Signal *s = new Signal( name, dir ); s->_peer = p; s->parameter_limits( min, max, default_value ); p->_signals.push_back( s ); MESSAGE( "Peer %s has created signal %s (%s %f %f %f)", p->name, name, direction, min, max, default_value ); if ( ep->_peer_signal_notification_callback ) ep->_peer_signal_notification_callback( s, Signal::Created, ep->_peer_signal_notification_userdata ); return 0; } int Endpoint::osc_sig_renamed ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { MESSAGE( "Got renamed message." ); const char *old_name = &argv[0]->s; const char *new_name = &argv[1]->s; Endpoint *ep = (Endpoint*)user_data; Peer *p = ep->find_peer_by_address( lo_message_get_source( msg ) ); if ( ! p ) { WARNING( "Got signal rename notification from unknown peer." ); return 0; } Signal *o = ep->find_peer_signal_by_path( p, old_name ); if ( ! o ) { WARNING( "Unknown signal: %s", old_name ); return 0; } MESSAGE( "Signal %s was renamed to %s", o->_path, new_name ); ep->rename_translation_source( o->_path, new_name ); free( o->_path ); o->_path = strdup( new_name ); return 0; } int Endpoint::osc_sig_handler ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { Signal *o; float f = 0.0; if ( ! strcmp( types, "f" ) ) { /* accept a value for signal named in path */ o = (Signal*)user_data; f = argv[0]->f; } else if ( ! types || 0 == types[0] ) { /* reply with current value */ o = (Signal*)user_data; o->_endpoint->send( lo_message_get_source( msg ), "/reply", path, o->value() ); return 0; } else { return -1; } o->_value = f; if ( o->_handler ) o->_handler( f, o->_user_data ); return 1; } const char** Endpoint::get_connections ( const char *path ) { const char ** conn = NULL; int j = 0; for ( std::map<std::string,TranslationDestination>::iterator i = _translations.begin(); i != _translations.end(); i++ ) { if ( !strcmp( i->second.path.c_str(), path ) ) { conn = (const char**)realloc( conn, sizeof( char * ) * (j+2)); conn[j++] = i->first.c_str(); } } if ( conn ) conn[j] = 0; return conn; } void Endpoint::clear_translations ( void ) { _translations.clear(); } void Endpoint::add_translation ( const char *a, const char *b ) { _translations[a].path = b; } void Endpoint::del_translation ( const char *a ) { std::map<std::string,TranslationDestination>::iterator i = _translations.find( a ); if ( i != _translations.end() ) _translations.erase( i ); } void Endpoint::rename_translation_destination ( const char *a, const char *b ) { for ( std::map<std::string,TranslationDestination>::iterator i = _translations.begin(); i != _translations.end(); i++ ) { if ( !strcmp( i->second.path.c_str(), a ) ) { i->second.path = b; } } } void Endpoint::rename_translation_source ( const char *a, const char *b ) { std::map<std::string,TranslationDestination>::iterator i = _translations.find( a ); if ( i != _translations.end() ) { _translations[b] = _translations[a]; _translations.erase( i ); } } int Endpoint::ntranslations ( void ) { return _translations.size(); } bool Endpoint::get_translation ( int n, const char **from, const char **to ) { int j = 0; for ( std::map<std::string,TranslationDestination>::const_iterator i = _translations.begin(); i != _translations.end(); i++, j++) { if ( j == n ) { *from = i->first.c_str(); *to = i->second.path.c_str(); return true; } } return false; } int Endpoint::osc_generic ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { // OSC_DMSG(); Endpoint *ep = (Endpoint*)user_data; if ( ep->_learning_path ) { ep->add_translation( path, ep->_learning_path ); MESSAGE( "Learned translation \"%s\" -> \"%s\"", path, ep->_learning_path ); free(ep->_learning_path); ep->_learning_path = NULL; return 0; } { std::map<std::string,TranslationDestination>::iterator i = ep->_translations.find( path ); if ( i != ep->_translations.end() ) { const char *dpath = i->second.path.c_str(); // MESSAGE( "Translating message \"%s\" to \"%s\"", path, dpath ); if ( !strcmp(types, "f" )) { // MESSAGE( "recording value %f", argv[0]->f ); i->second.current_value = argv[0]->f; } i->second.suppress_feedback = true; lo_send_message(ep->_addr, dpath, msg ); return 0; } } if ( argc || path[ strlen(path) - 1 ] != '/' ) return -1; for ( std::list<Method*>::const_iterator i = ep->_methods.begin(); i != ep->_methods.end(); ++i ) { if ( ! (*i)->path() ) continue; if (! strncmp( (*i)->path(), path, strlen(path) ) ) { /* asprintf( &stored_path, "%s (%s); %s", path, typespec, argument_description ); */ ((Endpoint*)user_data)->send( lo_message_get_source( msg ), "/reply", path, (*i)->path() ); } } ((Endpoint*)user_data)->send( lo_message_get_source( msg ), "/reply", path ); return 0; } int Endpoint::osc_signal_lister ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { // OSC_DMSG(); MESSAGE( "Listing signals." ); const char *prefix = NULL; if ( argc ) prefix = &argv[0]->s; Endpoint *ep = (Endpoint*)user_data; for ( std::list<Signal*>::const_iterator i = ep->_signals.begin(); i != ep->_signals.end(); ++i ) { Signal *o = *i; if ( ! prefix || ! strncmp( o->path(), prefix, strlen(prefix) ) ) { ep->send( lo_message_get_source( msg ), "/reply", path, o->path(), o->_direction == Signal::Input ? "in" : "out", o->parameter_limits().min, o->parameter_limits().max, o->parameter_limits().default_value ); } } ep->send( lo_message_get_source( msg ), "/reply", path ); return 0; } bool Endpoint::address_matches ( lo_address addr1, lo_address addr2 ) { char *purl = strdup( lo_address_get_port( addr1 ) ); char *url = strdup( lo_address_get_port( addr2 ) ); bool r = !strcmp( purl, url ); free( purl ); free( url ); return r; } void Endpoint::list_peer_signals ( void *v ) { for ( std::list<Peer*>::iterator i = _peers.begin(); i != _peers.end(); ++i ) { for ( std::list<Signal*>::iterator j = (*i)->_signals.begin(); j != (*i)->_signals.end(); ++j ) { if ( _peer_signal_notification_callback ) _peer_signal_notification_callback( *j, OSC::Signal::Created, v ); } } } Peer * Endpoint::find_peer_by_address ( lo_address addr ) { char *url = strdup( lo_address_get_port( addr ) ); Peer *p = NULL; for ( std::list<Peer*>::iterator i = _peers.begin(); i != _peers.end(); ++i ) { char *purl = strdup( lo_address_get_port( (*i)->addr ) ); if ( !strcmp( purl, url ) ) { free( purl ); p = *i; break; } free(purl); } free( url ); return p; } Peer * Endpoint::find_peer_by_name ( const char *name ) { for ( std::list<Peer*>::iterator i = _peers.begin(); i != _peers.end(); ++i ) { if ( !strcmp( name, (*i)->name ) ) { return *i; } } return NULL; } bool Endpoint::disconnect_signal ( OSC::Signal *s, const char *signal_path ) { if ( s->_direction == Signal::Output ) { for ( std::list<Peer*>::iterator i = _peers.begin(); i != _peers.end(); ++i ) { send( (*i)->addr, "/signal/disconnect", s->path(), signal_path); } return true; } return false; } bool Endpoint::connect_signal( OSC::Signal *s, const char *signal_path ) { if ( s->_direction == Signal::Output ) { for ( std::list<Peer*>::iterator i = _peers.begin(); i != _peers.end(); i++ ) { send( (*i)->addr, "/signal/connect", s->path(), signal_path ); } } return true; } int Endpoint::osc_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { Endpoint *ep = (Endpoint*)user_data; if ( argc && !strcmp( &argv[0]->s, "/signal/list" ) ) { Peer *p = ep->find_peer_by_address( lo_message_get_source( msg ) ); if ( ! p ) { WARNING( "Got input list reply from unknown peer." ); return 0; } if ( argc == 1 ) { p->_scanning = false; MESSAGE( "Done scanning %s", p->name ); if ( ep->_peer_scan_complete_callback ) ep->_peer_scan_complete_callback(ep->_peer_scan_complete_userdata); } else if ( argc == 6 && p->_scanning ) { Signal *s = ep->find_peer_signal_by_path( p, &argv[1]->s ); if ( s ) return 0; MESSAGE( "Peer %s has signal %s (%s)", p->name, &argv[1]->s, &argv[2]->s ); int dir = 0; if ( !strcmp( &argv[2]->s, "in" ) ) dir = Signal::Input; else if ( !strcmp( &argv[2]->s, "out" ) ) dir = Signal::Output; s = new Signal( &argv[1]->s, (Signal::Direction)dir ); s->_peer = p; s->parameter_limits( argv[3]->f, argv[4]->f, argv[5]->f ); p->_signals.push_back( s ); // ep->_signals.push_back(s); if ( ep->_peer_signal_notification_callback ) ep->_peer_signal_notification_callback( s, Signal::Created, ep->_peer_signal_notification_userdata ); } return 0; } else return -1; } Method * Endpoint::add_method ( const char *path, const char *typespec, lo_method_handler handler, void *user_data, const char *argument_description ) { // MESSAGE( "Added OSC method %s (%s)", path, typespec ); lo_server_add_method( _server, path, typespec, handler, user_data ); Method *md = new Method; if ( path ) md->_path = strdup( path ); if ( typespec ) md->_typespec = strdup( typespec ); if ( argument_description ) md->_documentation = strdup( argument_description ); _methods.push_back( md ); return md; } Signal * Endpoint::add_signal ( const char *path, Signal::Direction dir, float min, float max, float default_value, signal_handler handler, void *user_data ) { char *s; asprintf( &s, "%s%s", name() ? name() : "", path ); Signal *o = new Signal( s, dir ); free(s); o->_handler = handler; o->_user_data = user_data; o->_endpoint = this; o->parameter_limits( min, max, default_value ); _signals.push_back( o ); /* if ( dir == Signal::Input ) */ /* { */ lo_server_add_method( _server, o->_path, NULL, osc_sig_handler, o ); /* } */ /* tell our peers about it */ for ( std::list<Peer*>::iterator i = _peers.begin(); i != _peers.end(); ++i ) { send( (*i)->addr, "/signal/created", o->path(), o->_direction == Signal::Input ? "in" : "out", min, max, default_value ); } return o; } void Endpoint::del_method ( const char *path, const char *typespec ) { // MESSAGE( "Deleted OSC method %s (%s)", path, typespec ); lo_server_del_method( _server, path, typespec ); for ( std::list<Method *>::iterator i = _methods.begin(); i != _methods.end(); ++i ) { if ( ! (*i)->path() ) continue; if ( ! strcmp( path, (*i)->path() ) && ! strcmp( typespec, (*i)->typespec() ) ) { delete *i; i = _methods.erase( i ); break; } } } void Endpoint::del_method ( Method *meth ) { // MESSAGE( "Deleted OSC method %s (%s)", path, typespec ); lo_server_del_method( _server, meth->path(), meth->typespec() ); delete meth; _methods.remove( meth ); } void Endpoint::del_signal ( Signal *o ) { // MESSAGE( "Deleted OSC method %s (%s)", path, typespec ); lo_server_del_method( _server, o->path(), NULL ); /* tell our peers about it */ for ( std::list<Peer*>::iterator i = _peers.begin(); i != _peers.end(); ++i ) { send( (*i)->addr, "/signal/removed", o->path() ); } /* FIXME: clear loopback connections first! */ _signals.remove( o ); } /* prepare to learn a translation for /path/. The next unhandled message to come through will be mapped to /path/ */ void Endpoint::learn ( const char *path ) { if ( _learning_path ) free( _learning_path ); _learning_path = NULL; if ( path ) _learning_path = strdup( path ); } /** if there's a translation with a destination of 'path', then send feedback for it */ void Endpoint::send_feedback ( const char *path, float v ) { for ( std::map<std::string,TranslationDestination>::iterator i = _translations.begin(); i != _translations.end(); i++ ) { if ( path && ! strcmp( i->second.path.c_str(), path ) ) { /* found it */ if ( !i->second.suppress_feedback && i->second.current_value != v ) { const char *spath = i->first.c_str(); // MESSAGE( "Sending feedback to \"%s\": %f", spath, v ); /* send to all peers */ for ( std::list<Peer*>::iterator p = _peers.begin(); p != _peers.end(); ++p ) { send( (*p)->addr, spath, v ); } i->second.current_value = v; } i->second.suppress_feedback = false; /* break; */ } } } Peer * Endpoint::add_peer ( const char *name, const char *url ) { Peer *p = new Peer; MESSAGE( "Adding peer %s @ %s...", name, url ); p->name = strdup( name ); p->addr = lo_address_new_from_url( url ); _peers.push_back( p ); return p; } void Endpoint::scan_peer ( const char *name, const char *url ) { Peer *p = add_peer(name,url); p->_scanning = true; MESSAGE( "Scanning peer %s", name ); send( p->addr, "/signal/list" ); } void * Endpoint::osc_thread ( void * arg ) { ((Endpoint*)arg)->osc_thread(); return NULL; } void Endpoint::osc_thread ( void ) { _thread.name( "OSC" ); MESSAGE( "OSC Thread running" ); run(); } void Endpoint::start ( void ) { if ( !_thread.clone( &Endpoint::osc_thread, this ) ) FATAL( "Could not create OSC thread" ); /* lo_server_thread_start( _st ); */ } void Endpoint::stop ( void ) { _thread.join(); // lo_server_thread_stop( _st ); } int Endpoint::port ( void ) const { return lo_server_get_port( _server ); } char * Endpoint::url ( void ) const { return lo_server_get_url( _server ); } /** Process any waiting events and return immediately */ void Endpoint::check ( void ) const { wait( 0 ); } /** Process any waiting events and return after timeout */ void Endpoint::wait ( int timeout ) const { if ( lo_server_wait( _server, timeout ) ) while ( lo_server_recv_noblock( _server, 0 ) ) { } } /** Process events forever */ void Endpoint::run ( void ) const { for ( ;; ) { lo_server_recv( _server ); } } int Endpoint::send ( lo_address to, const char *path, std::list< OSC_Value > values ) { lo_message m = lo_message_new(); for ( std::list< OSC_Value >::const_iterator i = values.begin(); i != values.end(); ++i ) { const OSC_Value *ov = &(*i); switch ( ov->type() ) { case 'f': // MESSAGE( "Adding float %f", ((OSC_Float*)ov)->value() ); lo_message_add_float( m, ((OSC_Float*)ov)->value() ); break; case 'i': // MESSAGE( "Adding int %i", ((OSC_Int*)ov)->value() ); lo_message_add_int32( m, ((OSC_Int*)ov)->value() ); break; case 's': // MESSAGE( "Adding string %s", ((OSC_String*)ov)->value() ); lo_message_add_string( m, ((OSC_String*)ov)->value() ); break; default: FATAL( "Unknown format: %c", ov->type() ); break; } } // MESSAGE( "Path: %s", path ); lo_bundle b = lo_bundle_new( LO_TT_IMMEDIATE ); lo_bundle_add_message(b, path, m ); int r = lo_send_bundle_from( to, _server, b ); // int r = lo_send_message_from( to, _server, path, m ); // lo_message_free( m ); return r; } int Endpoint::send ( lo_address to, const char *path ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "" ); } int Endpoint::send ( lo_address to, const char *path, int v ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "i", v ); } int Endpoint::send ( lo_address to, const char *path, float v ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "f", v ); } int Endpoint::send ( lo_address to, const char *path, double v ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "d", v ); } int Endpoint::send ( lo_address to, const char *path, const char * v ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "s", v ); } int Endpoint::send ( lo_address to, const char *path, const char * v1, float v2 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sf", v1, v2 ); } int Endpoint::send ( lo_address to, const char *path, const char * v1, const char *v2 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ss", v1, v2 ); } int Endpoint::send ( lo_address to, const char *path, const char * v1, const char *v2, const char *v3 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sss", v1, v2, v3 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, int v3, int v4 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "siii", v1, v2, v3, v4 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, int v3, int v4, int v5 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ssiii", v1, v2, v3, v4, v5 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, int v4, int v5, int v6 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sssiii", v1, v2, v3, v4, v5, v6 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, int v2 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "si", v1, v2 ); } int Endpoint::send ( lo_address to, const char *path, int v1, const char *v2 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "is", v1, v2 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, const char *v3 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sis", v1, v2, v3 ); } int Endpoint::send ( lo_address to, const char *path, int v1, const char *v2, const char *v3, const char *v4 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "isss", v1, v2, v3, v4 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, const char *v3, const char *v4, const char *v5 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sisss", v1, v2, v3, v4, v5 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4, const char *v5 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sssss", v1, v2, v3, v4, v5 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ssss", v1, v2, v3, v4 ); } int Endpoint::send ( lo_address to, const char *path, lo_message msg ) { return lo_send_message_from( to, _server, path, msg ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, int v3, float v4, float v5, float v6 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ssifff", v1, v2, v3, v4, v5, v6 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, int v4, float v5, float v6, float v7 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sssifff", v1, v2, v3, v4, v5, v6, v7 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, float v4, float v5, float v6 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sssfff", v1, v2, v3, v4, v5, v6 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, int v3 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sii", v1, v2, v3 ); } int Endpoint::send ( lo_address to, const char *path, int v1, int v2 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ii", v1, v2 ); } int Endpoint::send ( lo_address to, const char *path, int v1, float v2 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "if", v1, v2 ); } int Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, int v3, float v4 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "siif", v1, v2, v3, v4 ); } int Endpoint::send ( lo_address to, const char *path, int v1, int v2, float v3 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "iif", v1, v2, v3 ); } } 07070100000034000081A40000000000000000000000016258A65C00003A87000000000000000000000000000000000000004000000000new-session-manager-1.6.0+git.20220415.0f6719c/src/Endpoint.hpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #pragma once #include <lo/lo.h> #include "Thread.hpp" #include <list> #include <string> #include <stdlib.h> #include <string.h> #include <map> namespace OSC { class OSC_Value { protected: char _type; float f; double d; int i; const char *s; public: OSC_Value ( const OSC_Value &rhs ) { _type = rhs._type; f =rhs.f; d = rhs.d; i = rhs.i; s = rhs.s; } OSC_Value ( ) { _type = 0; f = 0; d = 0; i = 0; s = 0; } virtual ~OSC_Value ( ) { } virtual char type ( void ) const { return _type; } }; class OSC_Float : public OSC_Value { public: float value ( void ) const { return f; } OSC_Float ( float v ) { _type = 'f'; f = v; } }; class OSC_Int : public OSC_Value { public: int value ( void ) const { return i; } OSC_Int ( int v ) { _type = 'i'; i = v; } }; class OSC_String : public OSC_Value { public: const char * value ( void ) const { return s; } OSC_String ( const char *v ) { _type = 's'; s = v; } }; struct Parameter_Limits { float min; float max; float default_value; }; class Endpoint; class Signal; struct Peer { bool _scanning; char *name; lo_address addr; std::list<Signal*> _signals; }; typedef int (*signal_handler) ( float value, void *user_data ); class Signal { // static int next_id; public: enum State { Created = 0, Removed = 1 }; enum Direction { Input, Output, Bidirectional }; private: Endpoint *_endpoint; Peer *_peer; char *_path; char *_documentation; float _value; Direction _direction; signal_handler _handler; void *_user_data; Parameter_Limits _parameter_limits; void (*_connection_state_callback)(OSC::Signal *, void*); void *_connection_state_userdata; public: const char * peer_name ( void ) const { return _peer->name; } Signal ( const char *path, Direction dir ); ~Signal ( ); Direction direction ( void ) const { return _direction; } void parameter_limits ( float min, float max, float default_value ) { _parameter_limits.min = min; _parameter_limits.max = max; _parameter_limits.default_value = default_value; _value = default_value; } void connection_state_callback ( void(*_cb)(OSC::Signal *, void*), void *userdata) { _connection_state_callback = _cb; _connection_state_userdata = userdata; } const Parameter_Limits& parameter_limits ( void ) const { return _parameter_limits; } const char *path ( void ) const { return _path; } void rename ( const char *name ); /* publishes value to targets */ void value ( float v ); /* get current value */ float value ( void ) const { return _value; } bool is_connected_to ( const Signal *s ) const; friend class Endpoint; }; class Method { char *_path; char *_typespec; char *_documentation; public: const char *path ( void ) { return _path; } const char *typespec ( void ) { return _typespec; } Method ( ); ~Method ( ); friend class Endpoint; }; class Endpoint { Thread _thread; friend class Signal; // lo_server_thread _st; lo_server _server; lo_address _addr; std::list<Peer*> _peers; std::list<Signal*> _signals; std::list<Method*> _methods; char *_learning_path; class TranslationDestination { public: std::string path; float current_value; bool suppress_feedback; TranslationDestination ( ) { suppress_feedback = false; current_value = -1.0f; } }; std::map<std::string,TranslationDestination> _translations; void (*_peer_scan_complete_callback)(void*); void *_peer_scan_complete_userdata; char *_name; static void error_handler(int num, const char *msg, const char *path); static int osc_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_signal_lister ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_generic ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_sig_handler ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_sig_renamed ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_sig_removed ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_sig_created ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_sig_disconnect ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_sig_connect ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); static int osc_sig_hello ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); Peer * add_peer ( const char *name, const char *url ); void scan_peer ( const char *name, const char *url ); private: static void *osc_thread ( void *arg ); void osc_thread ( void ); OSC::Signal *find_peer_signal_by_path ( Peer *p, const char *path ); OSC::Signal *find_signal_by_path ( const char *path ); Peer *find_peer_by_name ( const char *name ); Peer *find_peer_by_address ( lo_address addr ); static bool address_matches ( lo_address addr1, lo_address addr2 ); static Signal *find_target_by_peer_address ( std::list<Signal*> *l, lo_address addr ); void del_signal ( Signal *signal ); void send_signal_rename_notifications( Signal *s ); void (*_peer_signal_notification_callback)( OSC::Signal *, OSC::Signal::State, void*); void *_peer_signal_notification_userdata; public: void send_feedback ( const char *path, float v ); void learn ( const char *path ); lo_address address ( void ) { return _addr; } const char * * get_connections ( const char *path ); void clear_translations ( void ); void del_translation ( const char *a ); void add_translation ( const char *a, const char *b ); void rename_translation_destination ( const char *a, const char *b ); void rename_translation_source ( const char *a, const char *b ); int ntranslations ( void ); bool get_translation ( int n, const char **from, const char **to ); void peer_signal_notification_callback ( void (*cb)(OSC::Signal *, OSC::Signal::State, void*), void *userdata ) { _peer_signal_notification_callback = cb; _peer_signal_notification_userdata = userdata; } // can be used to point back to owning object. void *owner; void list_peer_signals ( void *v ); int init ( int proto, const char *port = 0 ); Endpoint ( ); ~Endpoint ( ); bool disconnect_signal ( OSC::Signal *s, OSC::Signal *d ); bool disconnect_signal ( OSC::Signal *s, const char *signal_path ); bool connect_signal ( OSC::Signal *s, OSC::Signal *d ); bool connect_signal ( OSC::Signal *s, const char *peer_name, const char *signal_path ); // bool connect_signal ( OSC::Signal *s, const char *peer_name, int signal_id ); bool connect_signal ( OSC::Signal *s, const char *peer_and_path ); Signal * add_signal ( const char *path, Signal::Direction dir, float min, float max, float default_value, signal_handler handler, void *user_data ); Method *add_method ( const char *path, const char *typespec, lo_method_handler handler, void *user_data, const char *argument_description ); void del_method ( const char *path, const char *typespec ); void del_method ( Method* method ); void start ( void ); void stop ( void ); int port ( void ) const; char * url ( void ) const; void check ( void ) const; void wait ( int timeout ) const; void run ( void ) const; void name ( const char *name ) { _name = strdup( name ); } const char *name ( void ) { return _name; } void hello ( const char *url ); void handle_hello ( const char *peer_name, const char *peer_url ); int send ( lo_address to, const char *path, std::list< OSC_Value > values ); /* overloads for common message formats */ int send ( lo_address to, const char *path ); int send ( lo_address to, const char *path, float v ); int send ( lo_address to, const char *path, double v ); int send ( lo_address to, const char *path, int v ); int send ( lo_address to, const char *path, long v ); int send ( lo_address to, const char *path, int v1, int v2 ); int send ( lo_address to, const char *path, int v1, float v2 ); int send ( lo_address to, const char *path, int v1, int v2, float v3 ); int send ( lo_address to, const char *path, const char *v ); int send ( lo_address to, const char *path, const char *v1, float v2 ); int send ( lo_address to, const char *path, const char *v1, int v2, int v3 ); int send ( lo_address to, const char *path, const char *v1, const char *v2 ); int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3 ); int send ( lo_address to, const char *path, const char *v1, int v2, int v3, int v4 ); int send ( lo_address to, const char *path, const char *v1, const char *v2, int v3, int v4, int v5 ); int send ( lo_address to, const char *path, const char *v1, int v2 ); int send ( lo_address to, const char *path, int v1, const char *v2 ); int send ( lo_address to, const char *path, const char *v1, int v2, int v3, float v4 ); int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, int v4, int v5, int v6 ); int send ( lo_address to, const char *path, const char *v1, int v2, const char *v3 ); int send ( lo_address to, const char *path, int v1, const char *v2, const char *v3, const char *v4 ); int send ( lo_address to, const char *path, const char *v1, int v2, const char *v3, const char *v4, const char *v5 ); int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4, const char *v5 ); int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4 ); int send ( lo_address to, const char *path, lo_message msg ); int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, float v4, float v5, float v6 ); int send ( lo_address to, const char *path, const char *v1, const char *v2, int v3, float v4, float v5, float v6 ); int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, int v4, float v5, float v6, float v7 ); void peer_scan_complete_callback ( void(*_cb)(void*), void *userdata) { _peer_scan_complete_callback = _cb; _peer_scan_complete_userdata = userdata; } friend Signal::~Signal(); friend void Signal::rename ( const char *name ); }; } /* helper macros for defining OSC handlers */ /* #define OSC_NAME( name ) osc_ ## name */ #define OSC_DMSG() MESSAGE( "Got OSC message: %s", path ); // #define OSC_HANDLER( name ) static int OSC_NAME( name ) ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) /* #define OSC_REPLY_OK() ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, "ok" ) */ /* #define OSC_REPLY( value ) ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, value ) */ /* #define OSC_REPLY_ERR() ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, "err" ) */ /* #define OSC_ENDPOINT() ((OSC::Endpoint*)user_data) */ 07070100000035000041ED0000000000000000000000026258A65C00000000000000000000000000000000000000000000003600000000new-session-manager-1.6.0+git.20220415.0f6719c/src/FL07070100000036000081A40000000000000000000000016258A65C00001B1D000000000000000000000000000000000000004800000000new-session-manager-1.6.0+git.20220415.0f6719c/src/FL/Fl_Packscroller.H /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ /* Scrolling group suitable for containing a single child (a * pack). When the Fl_Packscroller is resized, the child will be resized * too. No scrollbars are displayed, but the widget responds to * FL_MOUSEWHEEL events. */ #pragma once #include <FL/Fl_Group.H> #include <FL/fl_draw.H> #include <FL/Fl.H> /* FIXME: Optimize scroll */ class Fl_Packscroller : public Fl_Group { int _increment; int _yposition; static const int sbh = 15; /* scroll button height */ public: Fl_Packscroller ( int X, int Y, int W, int H, const char *L = 0 ) : Fl_Group( X, Y, W, H, L ) { _increment = 30; _yposition = 0; // color( FL_WHITE ); } int increment ( void ) const { return _increment; } void increment ( int v ) { _increment = v; } void yposition ( int v ) { if ( ! children() ) return; int Y = v; if ( Y > 0 ) Y = 0; const int H = h(); // - (sbh * 2); Fl_Widget *o = child( 0 ); if ( o->h() > H && Y + o->h() < H ) Y = H - o->h(); else if ( o->h() < H ) Y = 0; if ( _yposition != Y ) { _yposition = Y; damage( FL_DAMAGE_SCROLL ); } } int yposition ( void ) const { if ( children() ) return child( 0 )->y() - (y() + sbh); return 0; } void bbox ( int &X, int &Y, int &W, int &H ) { X = x(); Y = y() + ( sbh * top_sb_visible() ); W = w(); H = h() - ( sbh * ( top_sb_visible() + bottom_sb_visible() ) ); } int top_sb_visible ( void ) { return children() && child(0)->y() != y() ? 1 : 0; } int bottom_sb_visible ( void ) { if ( children() ) { Fl_Widget *o = child( 0 ); if ( o->h() > h() && o->y() + o->h() != y() + h() ) return 1; } return 0; } virtual void draw ( void ) { if ( damage() & FL_DAMAGE_ALL ) { fl_rectf( x(), y(), w(), h(), color() ); } if ( ! children() ) return; Fl_Widget *o = child( 0 ); o->position( x(), y() + _yposition ); const int top_sb = top_sb_visible(); const int bottom_sb = bottom_sb_visible(); fl_push_clip( x(), y() + ( sbh * top_sb ), w(), h() - (sbh * (top_sb + bottom_sb) )); draw_children(); fl_pop_clip(); fl_font( FL_HELVETICA, 12 ); if ( top_sb ) { fl_draw_box( box(), x(), y(), w(), sbh, color() ); fl_color( fl_contrast( FL_FOREGROUND_COLOR, color() ) ); fl_draw( "@2<", x(), y(), w(), sbh, FL_ALIGN_CENTER ); } if ( bottom_sb ) { fl_draw_box( box(), x(), y() + h() - sbh, w(), sbh, color() ); fl_color( fl_contrast( FL_FOREGROUND_COLOR, color() ) ); fl_draw( "@2>", x(), y() + h() - sbh, w(), sbh, FL_ALIGN_CENTER ); } } virtual int handle ( int m ) { switch ( m ) { case FL_PUSH: if ( top_sb_visible() && Fl::event_inside( x(), y(), w(), sbh ) ) { return 1; } else if ( bottom_sb_visible() && Fl::event_inside( x(), y() + h() - sbh, w(), sbh ) ) { return 1; } break; case FL_RELEASE: { if ( top_sb_visible() && Fl::event_inside( x(), y(), w(), sbh ) ) { yposition( yposition() + ( h() / 4 ) ); return 1; } else if ( bottom_sb_visible() && Fl::event_inside( x(), y() + h() - sbh, w(), sbh ) ) { yposition( yposition() - (h() / 4 ) ); return 1; } break; } case FL_KEYBOARD: { if ( Fl::event_key() == FL_Up ) { yposition( yposition() + ( h() / 4 ) ); return 1; } else if ( Fl::event_key() == FL_Down ) { yposition( yposition() - (h() / 4 ) ); return 1; } break; } case FL_MOUSEWHEEL: { yposition( yposition() - ( Fl::event_dy() * _increment ) ); return 1; } } return Fl_Group::handle( m ); } }; 07070100000037000081A40000000000000000000000016258A65C00001C36000000000000000000000000000000000000004500000000new-session-manager-1.6.0+git.20220415.0f6719c/src/FL/Fl_Scalepack.C /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ /* Fl_Scalepack This is similar to an Fl_Pack, but instead of the pack resizing itself to enclose its children, this pack resizes its children to fit itself. Of course, this only works well with highly flexible widgets, but the task comes up often enough to warrent this class. If and child happens to be the resizable() widget, then it will be resized so the all the other children can fit around it, with their current sizes (and the size of the Fl_Scalepack) maintained. NOTES: An Fl_Pack as a direct child will not work, because Fl_Pack changes its size in draw(), which throws off our resize calculation. The whole idea of widgets being able to resize themselves within draw() is horribly broken... */ #include "Fl_Scalepack.H" #include <FL/Fl.H> #include <FL/fl_draw.H> #include <stdio.h> Fl_Scalepack::Fl_Scalepack ( int X, int Y, int W, int H, const char *L ) : Fl_Group( X, Y, W, H, L ) { resizable( 0 ); _spacing = 0; } void Fl_Scalepack::resize ( int X, int Y, int W, int H ) { /* Fl_Group's resize will change our child widget sizes, which interferes with our own resizing method. */ long dx = X - x(); long dy = Y - y(); bool r = W != w() || H != h(); Fl_Widget::resize( X, Y, W, H ); Fl_Widget*const* a = array(); for (int i=children(); i--;) { Fl_Widget* o = *a++; o->position( o->x() + dx, o->y() + dy ); } if ( r ) redraw(); } void Fl_Scalepack::draw ( void ) { if ( resizable() == this ) /* this resizable( this ) is the default for Fl_Group and is * reset by Fl_Group::clear(), but it is not our default... */ resizable( 0 ); int tx = x() + Fl::box_dx( box() ); int ty = y() + Fl::box_dy( box() ); int tw = w() - Fl::box_dw( box() ); int th = h() - Fl::box_dh( box() ); if ( damage() & FL_DAMAGE_ALL ) { draw_box(); draw_label(); } int v = 0; int cth = 0; int ctw = 0; Fl_Widget * const * a = array(); for ( int i = children(); i--; ) { Fl_Widget *o = *a++; if ( o->visible() ) { ++v; if ( o != this->resizable() ) { cth += o->h(); ctw += o->w(); } cth += _spacing; ctw += _spacing; } } ctw -= _spacing; cth -= _spacing; if ( 0 == v ) return; if ( this->resizable() ) { int pos = 0; Fl_Widget * const * a = array(); for ( int i = children(); i--; ) { Fl_Widget *o = *a++; if ( o->visible() ) { int X, Y, W, H; if ( type() == HORIZONTAL ) { X = tx + pos; Y = ty; W = o->w(); H = th; } else { X = tx; Y = ty + pos; W = tw; H = o->h(); } if ( this->resizable() == o ) { if ( type() == HORIZONTAL ) W = tw - ctw - 3; else H = th - cth; } if (X != o->x() || Y != o->y() || W != o->w() || H != o->h() ) { o->resize(X,Y,W,H); o->clear_damage(FL_DAMAGE_ALL); } if ( damage() & FL_DAMAGE_ALL ) { draw_child( *o ); draw_outside_label( *o ); } else update_child( *o ); /* if ( this->resizable() == o ) */ /* fl_rect( o->x(), o->y(), o->w(), o->h(), type() == VERTICAL ? FL_GREEN : FL_BLUE ); */ if ( type() == HORIZONTAL ) pos += o->w() + spacing(); else pos += o->h() + spacing(); } } } else { int sz = 0; int pos = 0; if ( type() == HORIZONTAL ) sz = (tw - (_spacing * (v - 1))) / v; else sz = (th - (_spacing * (v - 1))) / v; Fl_Widget * const * a = array(); for ( int i = children(); i--; ) { Fl_Widget *o = *a++; if ( o->visible() ) { int X, Y, W, H; if ( type() == HORIZONTAL ) { X = tx + pos; Y = ty; W = sz; H = th; } else { X = tx; Y = ty + pos; W = tw; H = sz; } if (X != o->x() || Y != o->y() || W != o->w() || H != o->h() ) { o->resize(X,Y,W,H); o->clear_damage(FL_DAMAGE_ALL); } if ( damage() & FL_DAMAGE_ALL ) { draw_child( *o ); draw_outside_label( *o ); } else update_child( *o ); // fl_rect( o->x(), o->y(), o->w(), o->h(), type() == VERTICAL ? FL_RED : FL_YELLOW ); if ( type() == HORIZONTAL ) pos += o->w() + spacing(); else pos += o->h() + spacing(); } } } } 07070100000038000081A40000000000000000000000016258A65C000007D0000000000000000000000000000000000000004500000000new-session-manager-1.6.0+git.20220415.0f6719c/src/FL/Fl_Scalepack.H /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #pragma once #include <FL/Fl_Group.H> class Fl_Scalepack : public Fl_Group { int _spacing; public: enum { VERTICAL, HORIZONTAL }; Fl_Scalepack ( int X, int Y, int W, int H, const char *L = 0 ); virtual ~Fl_Scalepack ( ) { } int spacing ( void ) const { return _spacing; } void spacing ( int v ) { _spacing = v; redraw(); } virtual void resize ( int, int, int, int ); virtual void draw ( void ); }; 07070100000039000081A40000000000000000000000016258A65C00001290000000000000000000000000000000000000004300000000new-session-manager-1.6.0+git.20220415.0f6719c/src/NSM_Proxy_UI.fl# data file for the Fltk User Interface Designer (fluid) version 1.0305 header_name {.H} code_name {.C} class NSM_Proxy_UI {open } { Function {make_window()} {open } { Fl_Window {} { label {NSM Proxy} open xywh {720 472 675 505} type Double color 47 labelcolor 55 xclass {NSM-Proxy} visible } { Fl_Return_Button start_button { label Start xywh {285 460 88 25} } Fl_Button kill_button { label Kill xywh {150 460 80 25} color 72 hide } Fl_Tabs {} {open xywh {0 0 678 445} } { Fl_Group {} { label Run open xywh {25 46 635 359} } { Fl_Box {} { label {NSM-Proxy handles clients without direct NSM support. It should not be used to start real NSM clients. Command-line options MUST go in the "Arguments" field. The program will be started with its current working directory being a uniquely named directory under the current session directory. It is recommended that you only refer to files as arguments in this directory to guarantee a transportable and archivable session. } selected xywh {28 87 612 150} box BORDER_BOX color 41 labelfont 8 labelsize 12 labelcolor 55 align 128 } Fl_Input arguments_input { label {Arguments:} tooltip {Command line parameter like --verbose} xywh {170 285 345 25} } Fl_Input label_input { label {NSM GUI Label:} tooltip {Will show in your NSM GUI, useful if you have multiple NSM-Proxy clients in one session} xywh {170 324 345 26} } Fl_Input executable_input { label {Executable Name:} tooltip {The pure name of the executable. Just xterm not /usr/bin/xterm} xywh {170 249 345 26} } } Fl_Group {} { label Advanced open xywh {12 31 663 414} hide } { Fl_Choice save_signal_choice { label {Save Signal:} open xywh {246 400 170 25} down_box BORDER_BOX } { MenuItem {} { label None xywh {10 10 40 24} } MenuItem {} { label SIGUSR1 xywh {20 20 40 24} } MenuItem {} { label SIGUSR2 xywh {30 30 40 24} } MenuItem {} { label SIGINT xywh {40 40 40 24} } } Fl_Box {} { label {The environment variables $NSM_CLIENT_ID and $NSM_SESSION_NAME will contain the unique client ID (suitable for use as e.g. a JACK client name) and the display name for the session, respectively. The variable $CONFIG_FILE will contain the name of the nsm-proxy config file.} xywh {26 46 610 84} box BORDER_BOX color 41 labelfont 8 labelsize 12 labelcolor 55 align 128 } Fl_Box {} { label {Very few programs may respond to a specific Unix signal by somehow saving their state. If 'Save Signal' is set to something other than 'None', then NSM Proxy will deliver the specified signal to the proxied process upon an NSM 'Save' event. Most programs will treat these signals just like SIGTERM and die. NSM-Proxy cannot force a program to save in our session directory. } xywh {26 306 610 79} box BORDER_BOX color 41 labelfont 8 labelsize 12 labelcolor 55 align 128 } Fl_Choice stop_signal_choice { label {Stop Signal:} open xywh {246 255 170 25} down_box BORDER_BOX } { MenuItem {} { label SIGTERM xywh {20 20 40 24} } MenuItem {} { label SIGINT xywh {50 50 40 24} } MenuItem {} { label SIGHUP xywh {60 60 40 24} } } Fl_Box {} { label {Most programs will shutdown gracefully when sent a SIGTERM or SIGINT signal. It's impossible to know which signal a specific program will respond to. A unhandled signal will simply kill the process, and may cause problems with the audio subsystem (e.g. JACK). Check the program's documentation or source code to determine which signal to use to stop it gracefully.} xywh {26 166 610 79} box BORDER_BOX color 41 labelfont 8 labelsize 12 labelcolor 55 align 128 } Fl_File_Input config_file_input { label {(Hidden!) Config File:} xywh {158 409 406 31} hide } Fl_Button config_file_browse_button { label Browse xywh {573 410 85 25} hide } } } } } } 0707010000003A000081A40000000000000000000000016258A65C00000E16000000000000000000000000000000000000003E00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/Thread.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #include "Thread.hpp" #include <assert.h> #include <string.h> pthread_key_t Thread::_current = 0; Thread::Thread ( ) { _thread = 0; _running = false; _name = 0; } Thread::Thread ( const char *name ) { _thread = 0; _running = false; _name = name; } void Thread::init ( void ) { pthread_key_create( &_current, NULL ); } bool Thread::is ( const char *name ) { return ! strcmp( Thread::current()->name(), name ); } /** to be used by existing threads (that won't call clone()) */ void Thread::set ( const char *name ) { _thread = pthread_self(); _name = name; _running = true; pthread_setspecific( _current, (void*)this ); } Thread * Thread::current ( void ) { return (Thread*)pthread_getspecific( _current ); } struct thread_data { void *(*entry_point)(void *); void *arg; void *t; }; void * Thread::run_thread ( void *arg ) { thread_data td = *(thread_data *)arg; delete (thread_data*)arg; pthread_setspecific( _current, td.t ); ((Thread*)td.t)->_running = true; void * r = td.entry_point( td.arg ); ((Thread*)td.t)->_running = false; return r; } bool Thread::clone ( void *(*entry_point)(void *), void *arg ) { assert( ! _thread ); thread_data *td = new thread_data; td->entry_point = entry_point; td->arg = arg; td->t = this; if ( pthread_create( &_thread, NULL, run_thread, td ) != 0 ) return false; return true; } void Thread::detach ( void ) { pthread_detach( _thread ); _thread = 0; } void Thread::cancel ( void ) { pthread_cancel( _thread ); _thread = 0; } void Thread::join ( void ) { if ( _thread != 0 ) { /* not joined yet, go ahead. */ pthread_join( _thread, NULL ); } _thread = 0; } /* never call this unless some other thread will be calling 'join' on * this one, otherwise, running() will return true even though the * thread is dead */ void Thread::exit ( void *retval ) { _running = false; pthread_exit( retval ); } 0707010000003B000081A40000000000000000000000016258A65C000009FD000000000000000000000000000000000000003E00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/Thread.hpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #pragma once /* simple wrapper for pthreads with thread role checking */ #include <pthread.h> #define THREAD_ASSERT( n ) ASSERT( Thread::is( #n ), "Function called from wrong thread! (is %s, should be %s)", Thread::current()->name(), #n ) class Thread { static pthread_key_t _current; pthread_t _thread; const char * _name; volatile bool _running; static void * run_thread ( void *arg ); public: static bool is ( const char *name ); static void init ( void ); static Thread *current ( void ); Thread ( ); Thread ( const char *name ); const char *name ( void ) const { return _name; } void name ( const char *name ) { _name = name; } bool running ( void ) const { return _running; } void set ( const char *name ); void set ( void ) { set( _name ); } bool clone ( void *(*entry_point)(void *), void *arg ); void detach ( void ); void join ( void ); void cancel ( void ); void exit ( void *retval = 0 ); }; 0707010000003C000081A40000000000000000000000016258A65C00000A62000000000000000000000000000000000000003D00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/debug.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #include "debug.h" #include <string.h> #include <stdio.h> #include <stdarg.h> extern char * program_invocation_short_name; bool quietMessages = false; void warnf ( warning_t level, const char *module, const char *file, const char *function, int line, const char *fmt, ... ) { va_list args; static const char *level_tab[] = { "message", "", "warning", "", "assertion", "" }; module = program_invocation_short_name; if ( module ) fprintf( stderr, "[%s] ", module ); #ifndef NDEBUG if ( file ) fprintf( stderr, "%s", file ); if ( line ) fprintf( stderr, ":%i", line ); if ( function ) fprintf( stderr, " %s()", function ); fprintf( stderr, ": " ); #endif if ( unsigned( ( level << 1 ) + 1 ) < ( sizeof( level_tab ) / sizeof( level_tab[0] ) ) ) fprintf( stderr, "%s", level_tab[( level << 1 ) + 1] ); if ( fmt ) { va_start( args, fmt ); vfprintf( stderr, fmt, args ); va_end( args ); } fprintf( stderr, "\n" ); } 0707010000003D000081A40000000000000000000000016258A65C000011A3000000000000000000000000000000000000003B00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/debug.h /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ /* debug.h * * 11/21/2003 - Jonathan Moore Liles * * Debuging support. * * Disable by defining the preprocessor variable NDEBUG prior to inclusion. * * The following macros sould be defined as string literals * * name value * * __MODULE__ Name of module. eg. "libfoo" * * __FILE__ Name of file. eg. "foo.c" * * __FUNCTION__ Name of enclosing function. eg. "bar" * * (inteter literal) * __LINE__ Number of enclosing line. * * * __FILE__, and __LINE__ are automatically defined by standard CPP * implementations. __FUNCTION__ is more or less unique to GNU, and isn't * strictly a preprocessor macro, but rather a reserved word in the compiler. * There is a sed script available with this toolset that is able to fake * __FUNCTION__ (among other things) with an extra preprocesessing step. * * __MODULE__ is nonstandard and should be defined the enclosing program(s). * Autoconf defines PACKAGE as the module name, and these routines will use its * value instead if __MODULE__ is undefined. * * The following routines are provided (as macros) and take the same arguments * as printf(): * * MESSAGE( const char *format, ... ) * WARNING( const char *format, ... ) * FATAL( const char *format, ... ) * * Calling MESSAGE or WARNING prints the message to stderr along with module, * file and line information, as well as appropriate emphasis. Calling * FATAL will do the same, and then call abort() to end the program. It is * unwise to supply any of these marcros with arguments that produce side * effects. As, doing so will most likely result in Heisenbugs; program * behavior that changes when debugging is disabled. * */ #ifndef _DEBUG_H #define _DEBUG_H #ifndef __MODULE__ #ifdef PACKAGE #define __MODULE__ PACKAGE #else #define __MODULE__ NULL #endif #endif #ifndef __GNUC__ #define __FUNCTION__ NULL #endif extern bool quietMessages; typedef enum { W_MESSAGE = 0, W_WARNING, W_FATAL } warning_t; void warnf ( warning_t level, const char *module, const char *file, const char *function, int line, const char *fmt, ... ); //We do not use NDEBUG anymore. Messages are a command line switch. //Warnings, asserts and errors are always important. // #define MESSAGE( fmt, args... ) warnf( W_MESSAGE, __MODULE__, __FILE__, __FUNCTION__, __LINE__, fmt, ## args ) #define MESSAGE( fmt, args... ) do { if ( ! (quietMessages) ) { warnf( W_MESSAGE, __MODULE__, __FILE__, __FUNCTION__, __LINE__, fmt, ## args ); } } while ( 0 ) #define WARNING( fmt, args... ) warnf( W_WARNING, __MODULE__, __FILE__, __FUNCTION__, __LINE__, fmt, ## args ) #define FATAL( fmt, args... ) ( warnf( W_FATAL, __MODULE__, __FILE__, __FUNCTION__, __LINE__, fmt, ## args ), abort() ) #define ASSERT( pred, fmt, args... ) do { if ( ! (pred) ) { warnf( W_FATAL, __MODULE__, __FILE__, __FUNCTION__, __LINE__, fmt, ## args ); abort(); } } while ( 0 ) #endif 0707010000003E000081A40000000000000000000000016258A65C00001413000000000000000000000000000000000000003C00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/file.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/vfs.h> unsigned long modification_time ( const char *file ) { struct stat st; if ( stat( file, &st ) ) return 0; return st.st_mtime; } /** returns /true/ if /file1/ is newer than /file2/ (or file2 doesn't exist) */ bool newer ( const char *file1, const char *file2 ) { return modification_time( file1 ) > modification_time( file2 ); } unsigned long size ( const char *file ) { struct stat st; if ( stat( file, &st ) ) return 0; return st.st_size; } int exists ( const char *name ) { struct stat st; return 0 == stat( name, &st ); } char * simple_hash( const char *s ) { //djb2 unsigned long hashAddress = 5381; for ( int counter = 0; s[counter]!='\0'; counter++ ) { hashAddress = ( (hashAddress << 5) + hashAddress ) + s[counter]; } char *result = NULL; asprintf( &result, "%lu", hashAddress % 65521 ); return result; } int backwards_fgetc ( FILE *fp ) { int c; if ( fseek( fp, -1, SEEK_CUR ) != 0 ) return -1; c = fgetc( fp ); fseek( fp, -1, SEEK_CUR ); return c; } char * backwards_afgets ( FILE *fp ) { size_t size = 0; char *s = NULL; size_t i = 0; int c; while ( ( c = backwards_fgetc( fp ) ) >= 0 ) { if ( i > 0 && '\n' == c ) break; if ( i >= size ) { size += 256; s = (char*)realloc( s, size ); } s[i++] = c; } if ( s ) { s[i] = 0; int len = strlen(s) ; int c, i, j; for (i = 0, j = len - 1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } fseek( fp, 1, SEEK_CUR ); return s; } /** update the modification time of file referred to by /fd/ */ void touch ( int fd ) { struct stat st; fstat( fd, &st ); fchmod( fd, st.st_mode ); } /** write a single string to a file */ void write_line ( const char *dir, const char *name, const char *value ) { char path[512]; snprintf( path, sizeof( path ), "%s/%s", dir, name ); FILE *fp = fopen( path, "w" ); if ( ! fp ) return; fputs( value, fp ); fclose( fp ); } /** read a single string from a file */ char * read_line ( const char *dir, const char *name ) { char path[512]; snprintf( path, sizeof( path ), "%s/%s", dir, name ); FILE *fp = fopen( path, "r" ); if ( ! fp ) return 0; char *value = (char*)malloc( 512 ); if ( ! fgets( value, 512, fp ) ) value[0] = 0; fclose( fp ); return value; } #include <sys/statvfs.h> /** return the number of blocks free on filesystem containing file named /file/ */ fsblkcnt_t free_space ( const char *file ) { struct statfs st; memset( &st, 0, sizeof( st ) ); statfs( file, &st ); return st.f_bavail; } /** return the total number of blocks on filesystem containing file named /file/ */ fsblkcnt_t total_space ( const char *file ) { struct statfs st; memset( &st, 0, sizeof( st ) ); statfs( file, &st ); return st.f_blocks; } /** return the percentage of usage on filesystem containing file named /file/ */ int percent_used ( const char *file ) { const double ts = total_space( file ); const double fs = free_space( file ); double percent_free = ( ( fs / ts ) * 100.0f ); return (int) (100.0f - percent_free); } 0707010000003F000081A40000000000000000000000016258A65C0000085F000000000000000000000000000000000000003A00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/file.h /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #include <stdio.h> unsigned long modification_time ( const char *file ); bool newer ( const char *file1, const char *file2 ); unsigned long size ( const char *file ); int exists ( const char *name ); char * simple_hash( const char *s); int backwards_fgetc ( FILE *fp ); char * backwards_afgets ( FILE *fp ); void touch ( int fd ); void write_line ( const char *dir, const char *name, const char *value ); char * read_line ( const char *dir, const char *name ); size_t free_space ( const char *file ); size_t total_space ( const char *file ); int percent_used ( const char *file ); 07070100000040000081A40000000000000000000000016258A65C00006C54000000000000000000000000000000000000003F00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/jackpatch.c /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ /* jackpatch.c This program is just like ASSPatch, except that it works with Jack ports (audio and MIDI). */ #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wunused-result" /* needed for asprintf */ #define _GNU_SOURCE #include <string.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <signal.h> #include <getopt.h> #include <sys/types.h> #include <sys/time.h> #include <errno.h> #include <stdlib.h> #include <jack/jack.h> #include <getopt.h> #include <lo/lo.h> #include <jack/ringbuffer.h> int client_active = 0; jack_client_t *client; lo_server losrv; lo_address nsm_addr; int nsm_is_active; char *project_file; int REAL_JACK_PORT_NAME_SIZE; //defined after jack client activated #undef VERSION #define APP_TITLE "JACKPatch" #define VERSION "1.0.0" struct patch_record { struct { char *client; char *port; } src , dst; int active; /* true if patch has already been activated (by us) */ struct patch_record *next; }; struct port_record { char *port; struct port_record *next; }; struct port_notification_record { int len; int reg; /* true if registered, false if unregistered */ char port[]; }; static struct port_record *known_ports = NULL; static struct patch_record *patch_list = NULL; static jack_ringbuffer_t *port_ringbuffer = NULL; /** * Pretty-print patch relationship of /pr/ */ void print_patch ( struct patch_record *pr, int mode ) { printf( "[jackpatch] %s from '%s:%s' to '%s:%s'\n", mode ? ">>" : "::", pr->src.client, pr->src.port, pr->dst.client, pr->dst.port ); } void enqueue ( struct patch_record *p ) { p->next = patch_list; patch_list = p; } void dequeue ( struct patch_record *pr ) { if ( !pr ) return; free( pr->src.port ); free( pr->dst.port ); free( pr->src.client ); free( pr->dst.client ); free( pr ); } void enqueue_port ( struct port_record **q, const char *port ) { struct port_record *p = malloc( sizeof( struct port_record )); p->port = strdup( port ); p->next = *q; *q = p; } void enqueue_known_port ( const char *port ) { enqueue_port( &known_ports, port ); } /** * Find a jack port in our own data structure (and not in the jack graph) */ const char * find_known_port ( const char *port ) { struct port_record *pr; for ( pr = known_ports; pr; pr = pr->next ) if ( !strcmp( port, pr->port ) ) return pr->port; return NULL; } /** * Convert a symbolic string of a jack connection into actual data struct patch_record * Argument example: * "PulseAudio JACK Sink:front-left |> system:playback_1" */ int process_patch ( const char *patch ) { struct patch_record *pr; char *leftc, *rightc, *leftp, *rightp; char dir[3]; int retval; retval = sscanf( patch, " %m[^:]:%m[^|] |%1[<>|] %m[^:]:%m[^\n]", &leftc, &leftp, dir, &rightc, &rightp ); if ( retval == EOF ) return -1; if ( retval != 5 ) return 0; /* trim space */ int j; for ( j = strlen( leftp ) - 1; j > 0; j-- ) { if ( leftp[j] == ' ' || leftp[j] == '\t' ) leftp[j] = 0; else break; } dir[2] = 0; pr = malloc( sizeof( struct patch_record ) ); switch ( *dir ) { case '<': pr->src.client = rightc; pr->src.port = rightp; pr->dst.client = leftc; pr->dst.port = leftp; enqueue( pr ); break; case '>': pr->src.client = leftc; pr->src.port = leftp; pr->dst.client = rightc; pr->dst.port = rightp; enqueue( pr ); break; case '|': pr->src.client = rightc; pr->src.port = rightp; pr->dst.client = leftc; pr->dst.port = leftp; enqueue( pr ); pr = malloc( sizeof( struct patch_record ) ); pr->src.client = strdup( leftc ); pr->src.port = strdup( leftp ); pr->dst.client = strdup( rightc ); pr->dst.port = strdup( rightp ); enqueue( pr ); break; default: // fprintf( stderr, "[jackpatch] Invalid token '|%s' at line %i of %s!", dir, i, file ); free( pr ); return 0; } pr->active = 0; //print_patch( pr, 1 ); //very verbose return 1; } void clear_all_patches ( ) { struct patch_record *pr; while ( patch_list ) { pr = patch_list; patch_list = pr->next; dequeue( pr ); } } /** * Crudely parse configuration file named by /file/ using fscanf */ int read_config ( const char *file ) { printf( "[jackpatch] Reading connections from file %s \n", file); FILE *fp; int i = 0; if ( NULL == ( fp = fopen( file, "r" ) ) ) return 0; clear_all_patches(); while ( !feof( fp ) && !ferror( fp ) ) { int retval; unsigned int k; char buf[BUFSIZ]; i++; for ( k = 0; k < sizeof( buf ) - 1; k++ ) { retval = fread( buf + k, 1, 1, fp ); if ( retval != 1 ) break; if ( buf[k] == '\n' ) { if ( k == 0 ) continue; else break; } } if ( retval == 0 ) break; retval = process_patch( buf ); if ( retval < 0 ) break; if ( retval == 0 ) { printf( "[jackpatch] bad line %i.\n", i ); continue; } } fclose( fp ); return 1; } /** A connection attempt will only be made when a JACK port registers itself and we receive * the jack callback, and once on startup. * There is no periodic check if a previously saved connection is still alive (by design!). * * returns 0 if connection failed, true if succeeded. Already connected * is not considered failure */ void connect_path ( struct patch_record *pr ) { int r = 0; char srcport[512]; // This should really be REAL_JACK_PORT_NAME_SIZE, but in the real world not every system and compiler does C99. char dstport[512]; snprintf( srcport, REAL_JACK_PORT_NAME_SIZE, "%s:%s", pr->src.client, pr->src.port ); snprintf( dstport, REAL_JACK_PORT_NAME_SIZE, "%s:%s", pr->dst.client, pr->dst.port ); if ( pr->active ) { /* patch is already active, don't bother JACK with it... */ return; } if ( ! ( find_known_port( srcport ) && find_known_port( dstport ) ) ) { /* * Since we only connect KNOWN TO US ports a connection will not be made on startup / file load, * even if both jack ports are actually present in JACK. * This is because we have not parsed both ports yet. * The true connection attempt will be made only when the second port of the pair was parsed. * * The log message below is misleading for users, because nothing is wrong, and should only * be used during development. * * We just skip the first attempt, eventhough JACK will not complain and do nothing wrong. * * That also means that we do not detect actually missing ports. */ //printf( "[jackpatch] Not attempting connection because one of the ports is missing: %s %s\n", srcport, dstport ); return; } // printf( "[jackpatch] Connecting %s |> %s\n", srcport, dstport ); r = jack_connect( client, srcport, dstport ); //print_patch( pr, r ); //very verbose if ( r == 0 || r == EEXIST ) { pr->active = 1; return; } else { pr->active = 0; printf( "[jackpatch] Error is %i\n", r ); return; } } void do_for_matching_patches ( const char *portname, void (*func)( struct patch_record * ) ) { struct patch_record *pr; char client[512]; //Linux jack limit is 64 char port[512]; //linux jack limit is 256 sscanf( portname, "%[^:]:%[^\n]", client, port ); for ( pr = patch_list; pr; pr = pr->next ) { if ( ( !strcmp( client, pr->src.client ) && !strcmp( port, pr->src.port ) ) || ( !strcmp( client, pr->dst.client ) && !strcmp( port, pr->dst.port ) ) ) { func( pr ); } } } void inactivate_path ( struct patch_record *pr ) { pr->active = 0; } void inactivate_patch ( const char *portname ) { do_for_matching_patches( portname, inactivate_path ); } void activate_patch ( const char *portname ) { do_for_matching_patches( portname, connect_path ); } void remove_known_port ( const char *port ) { /* remove it from the list of known ports */ { struct port_record *pr; struct port_record *lp = NULL; for ( pr = known_ports; pr; lp = pr, pr = pr->next ) if ( !strcmp( port, pr->port ) ) { if ( lp ) lp->next = pr->next; else known_ports = pr->next; free( pr->port ); free( pr ); break; } } /* now mark all patches including this port as inactive */ inactivate_patch ( port ); } /** * Called for every new port, which includes restored-from-file ports on startup. * It will try to activate a restored connection for every single port, thus doing an attempt * twice: Once for the source-port and then for the destination-port. */ void handle_new_port ( const char *portname ) { enqueue_known_port( portname ); //Verbose output that a new port was detected during runtime //printf( "[jackpatch] New endpoint '%s' registered.\n", portname ); /* this is a new port */ activate_patch( portname ); } void register_prexisting_ports ( void ) { const char **port; const char **ports = jack_get_ports( client, NULL, NULL, 0 ); if ( ! ports ) return; for ( port = ports; *port; port++ ) { handle_new_port( *port ); } free( ports ); } static int stringsort ( const void *a, const void *b ) { return strcmp(* (char * const *) a, * (char * const *) b); } /** * Save all current connections to a file. * * Strategy: * If there are no jack ports at all don't do anything. Else: * * Remember all currently known connections where one, or both, ports are missing from the jack graph. * We consider these temporarily gone by accident. * * Clear the current save file. * * For each currently existing jack output port save all of it's connections. * Save all these port-pairs in an empty file. Ports without connections are not saved. ** */ void snapshot ( const char *file ) { FILE *fp; const char **port; const char **ports = jack_get_ports( client, NULL, NULL, JackPortIsOutput ); if ( ! ports ) return; if ( NULL == ( fp = fopen( file, "w" ) ) ) { fprintf( stderr, "[jackpatch] Error opening snapshot file for writing\n" ); return; } //Prepare a temporary table where all connection strings are held until the file is written at the bottom of this function. //We first add all connections that are temporarily out of order (see below) and then all currently existing connections. const int table_increment = 16; int table_index = 0; size_t table_size = table_increment; char **table = (char**)malloc( sizeof( char * ) * table_increment ); //Before we forget the current state find all connections that we have in memory but where //one or both ports are currently missing in the jack graph. //We don't want to lose connections that are just temporarily not present. struct patch_record *pr; while ( patch_list ) { //A patch is one connection between a source and a destination. //If an actual jack port source is connected to more than one destinations it will appear as it's own "patch" in this list. //We only need to consider 1:1 point connections in this loop. //Traverse the linked list pr = patch_list; patch_list = pr->next; int remember_this_connection = 0; char * src_client_port; char * dst_client_port; asprintf( &src_client_port, "%s:%s", pr->src.client, pr->src.port ); asprintf( &dst_client_port, "%s:%s", pr->dst.client, pr->dst.port ); jack_port_t *jp_t_src; jp_t_src = jack_port_by_name( client, src_client_port ); //client is our own jackpatch-jack-client. if ( ! jp_t_src ) { //The port does not exist anymore. We need to remember it! //It doesn't matter if the destination port still exists, the file-writing below will only consider ports that are currently present and connected. //printf("[jackpatch] We remember source %s but it does not exist anymore. Making sure it will not be forgotten.\n", src_client_port); remember_this_connection = 1; } else { //The source port does still exist, but is it's connection still alive? //Do not use jack_port_get_all_connections, we want to know if a specific destination is still there. jack_port_t *jp_t_dst; jp_t_dst = jack_port_by_name( client, dst_client_port ); //client is our own jackpatch-jack-client. if ( ! jp_t_dst ) { //The port does not exist anymore. We need to remember it! //It doesn't matter if the destination port still exists, the file-writing below will only consider ports that are currently present and connected. //printf("[jackpatch] We remember destination %s but it does not exist anymore. Making sure it will not be forgotten.\n", dst_client_port); remember_this_connection = 1; } } if ( remember_this_connection ) { //const char * pport = src_client_port; //const char * pclient = dst_client_port; //This code is replicated below #TODO: create function. char *s; asprintf( &s, "%-40s |> %s\n", src_client_port, dst_client_port ); //prepare the magic string that is the step before creating a struct from with process_patch //port is source client:port and connection is the destination one. if ( table_index >= table_size ) { table_size += table_increment; table = (char**)realloc( table, table_size * sizeof( char *) ); } table[table_index++] = s; // process_patch( s ); infinite loop! But we still need to keep these patch_records! See below // Verbose output that an individual connection was saved. //printf( "[jackpatch] Remember ++ %s |> %s\n", src_client_port, dst_client_port ); } free ( src_client_port ); free ( dst_client_port ); } clear_all_patches(); //Tabula Rasa. //We just removed the patch_records we wanted to remember. //The last table_index holds the number of remembered records. for ( int record=0; record < table_index; record++ ) { process_patch ( table[record] ); } for ( port = ports; *port; port++ ) { //*port is a full client:port jack name, not only the port. jack_port_t *p; p = jack_port_by_name( client, *port ); const char **connections; const char **connection; connections = jack_port_get_all_connections( client, p ); if ( ! connections ) continue; for ( connection = connections; *connection; connection++ ) { //This code is replicated above #TODO: create function. char *s; asprintf( &s, "%-40s |> %s\n", *port, *connection ); //prepare the magic string that is the step before creating a struct from with process_patch //port is source client:port and connection is the destination one. if ( table_index >= table_size ) { table_size += table_increment; table = (char**)realloc( table, table_size * sizeof( char *) ); } table[table_index++] = s; process_patch( s ); // Verbose output that an individual connection was saved. //printf( "[jackpatch] ++ %s |> %s\n", *port, *connection ); } free( connections ); } free( ports ); qsort( table, table_index, sizeof(char*), stringsort ); int i; for ( i = 0; i < table_index; i++ ) { fprintf( fp, "%s", table[i] ); free(table[i]); } free(table); fclose( fp ); } static int die_now = 0; void signal_handler ( int x ) { printf("[jackpatch] Handle signal %d\n", x); die_now = 1; } void die ( void ) { if ( client_active ) jack_deactivate( client ); printf( "[jackpatch] Closing jack client\n" ); jack_client_close( client ); client = NULL; exit( 0 ); } /** set_traps * * Handle signals */ void set_traps ( void ) { signal( SIGHUP, signal_handler ); signal( SIGINT, signal_handler ); // signal( SIGQUIT, signal_handler ); // signal( SIGSEGV, signal_handler ); // signal( SIGPIPE, signal_handler ); signal( SIGTERM, signal_handler ); } /****************/ /* OSC HANDLERS */ /****************/ int osc_announce_error ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { if ( strcmp( types, "sis" ) ) return -1; if ( strcmp( "/nsm/server/announce", &argv[0]->s ) ) return -1; printf( "[jackpatch] Failed to register with NSM: %s\n", &argv[2]->s ); nsm_is_active = 0; return 0; } int osc_announce_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { if ( strcmp( "/nsm/server/announce", &argv[0]->s ) ) return -1; printf( "[jackpatch] Successfully registered. NSM says: %s\n", &argv[1]->s ); nsm_is_active = 1; nsm_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) ) ); return 0; } int osc_save ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { snapshot( project_file ); lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" ); return 0; } void maybe_activate_jack_client ( void ) { if ( ! client_active ) { jack_activate( client ); client_active = 1; REAL_JACK_PORT_NAME_SIZE = jack_port_name_size(); //global. This is client+port+1. 64 + 256 + 1 = 321 on Linux. } } int osc_open ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { const char *new_path = &argv[0]->s; // const char *display_name = &argv[1]->s; char *new_filename; asprintf( &new_filename, "%s.jackpatch", new_path ); struct stat st; if ( 0 == stat( new_filename, &st ) ) { if ( read_config( new_filename ) ) { /* wipe_ports(); */ /* check_for_new_ports(); */ maybe_activate_jack_client(); register_prexisting_ports(); } else { lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/error", "sis", path, -1, "Could not open file" ); return 0; } } else { maybe_activate_jack_client(); clear_all_patches(); } if ( project_file ) free( project_file ); project_file = new_filename; lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" ); return 0; } void announce ( const char *nsm_url, const char *client_name, const char *process_name ) { printf( "[jackpatch] Announcing to NSM\n" ); lo_address to = lo_address_new_from_url( nsm_url ); int pid = (int)getpid(); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/server/announce", "sssiii", client_name, ":switch:", process_name, 0, /* api_major_version */ 8, /* api_minor_version */ pid ); lo_address_free( to ); } void init_osc ( const char *osc_port ) { losrv = lo_server_new( osc_port, NULL ); //error_handler ); char *url = lo_server_get_url(losrv); printf("[jackpatch] OSC: %s\n",url); free(url); lo_server_add_method( losrv, "/nsm/client/save", "", osc_save, NULL ); lo_server_add_method( losrv, "/nsm/client/open", "sss", osc_open, NULL ); lo_server_add_method( losrv, "/error", "sis", osc_announce_error, NULL ); lo_server_add_method( losrv, "/reply", "ssss", osc_announce_reply, NULL ); } struct port_notification_record * dequeue_new_port ( void ) { int size = 0; if ( sizeof( int ) == jack_ringbuffer_peek( port_ringbuffer, (char*)&size, sizeof( int ) ) ) { if ( jack_ringbuffer_read_space( port_ringbuffer ) >= size ) { struct port_notification_record *pr = malloc( size ); jack_ringbuffer_read( port_ringbuffer, (char*)pr, size ); return pr; } } return NULL; } void check_for_new_ports ( void ) { struct port_notification_record *p = NULL; while ( ( p = dequeue_new_port() ) ) { if ( p->reg ) handle_new_port( p->port ); else remove_known_port( p->port ); free( p ); } } void port_registration_callback( jack_port_id_t id, int reg, void *arg ) { jack_port_t *p = jack_port_by_id( client, id ); const char *port = jack_port_name( p ); int size = strlen(port) + 1 + sizeof( struct port_notification_record ); struct port_notification_record *pr = malloc( size ); pr->len = size; pr->reg = reg; strcpy( pr->port, port ); if ( size != jack_ringbuffer_write( port_ringbuffer, (const char *)pr, size ) ) { fprintf( stderr, "[jackpatch] ERROR: port notification buffer overrun\n" ); } // enqueue_new_port( port, reg ); } /* */ int main ( int argc, char **argv ) { //Command line parameters static struct option long_options[] = { { "help", no_argument, 0, 'h' }, { "save", no_argument, 0, 's' }, { "version", no_argument, 0, 'v' }, { 0, 0, 0, 0 } }; int option_index = 0; int c = 0; while ( ( c = getopt_long_only( argc, argv, "", long_options, &option_index ) ) != -1 ) { switch ( c ) { case 'h': { const char *usage = "jackpatch - Remember and restore the JACK Audio Connection Kit Graph in NSM\n\n" "It is intended as module for the 'New Session Manager' and only communicates\n" "over OSC in an NSM-Session.\n\n" "It has limited standalone functionality for testing and debugging, such as:\n" "\n" "Usage:\n" " jackpatch [filename] Restore a snapshot from --save and monitor.\n" " jackpatch --help\n" "\n" "Options:\n" " --help Show this screen and exit\n" " --version Show version and exit\n" " --save Save current connection snapshot to file and exit\n" ""; puts ( usage ); exit(0); break; } case 's': { //save is handled below. break; } case 'v': { printf("%s\n", VERSION); exit(0); break; } } } jack_status_t status; client = jack_client_open( APP_TITLE, JackNullOption, &status ); jack_set_port_registration_callback( client, port_registration_callback, NULL ); if ( ! client ) { fprintf( stderr, "[jackpatch] Could not register JACK client\n" ); exit(1); } port_ringbuffer = jack_ringbuffer_create( 1024 * 8 ); set_traps(); if ( argc > 1 ) { maybe_activate_jack_client(); if ( ! strcmp( argv[1], "--save" ) ) { if ( argc > 2 ) { //To not discard temporarily missing clients we need to load the current ones from file first. if ( read_config( argv[2] ) ) // --save parameter { register_prexisting_ports(); } printf( "[jackpatch] Standalone: Saving current graph to: %s\n", argv[2] ); snapshot( argv[2] ); die(); } } else { /** * Enter standalone commandline mode. This is without NSM. */ if ( read_config( argv[1] ) ) { maybe_activate_jack_client(); register_prexisting_ports(); } printf( "[jackpatch] Monitoring in standalone mode…\n" ); for ( ;; ) { usleep( 50000 ); if ( die_now ) die(); check_for_new_ports(); } } } init_osc( NULL ); const char *nsm_url = getenv( "NSM_URL" ); if ( nsm_url ) { announce( nsm_url, APP_TITLE, argv[0] ); } else { fprintf( stderr, "[jackpatch] Could not register as NSM client.\n" ); exit(1); } for ( ;; ) { lo_server_recv_noblock( losrv, 200 ); if ( client_active ) check_for_new_ports(); if ( die_now ) die(); } } 07070100000041000081A40000000000000000000000016258A65C00002616000000000000000000000000000000000000004100000000new-session-manager-1.6.0+git.20220415.0f6719c/src/jackpatch.svg<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="68.791656mm" height="68.791656mm" viewBox="0 0 68.791656 68.791656" version="1.1" id="svg8" sodipodi:docname="icons.svg" inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> <defs id="defs2" /> <sodipodi:namedview fit-margin-bottom="0" fit-margin-right="0" fit-margin-left="0" fit-margin-top="0" units="px" id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.98994949" inkscape:cx="-402.6148" inkscape:cy="-222.59193" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" showgrid="false" inkscape:window-width="2558" inkscape:window-height="1398" inkscape:window-x="0" inkscape:window-y="20" inkscape:window-maximized="1" /> <metadata id="metadata5"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g transform="translate(-230.22857,-195.0233)" inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"> <g id="nsm" inkscape:label="nsm" transform="translate(-172.56864)"> <title id="title888">nsm</title> <rect y="114.63333" x="71.133331" height="67.73333" width="67.73333" id="rect10" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="105.32493" y="169.25755" id="text852"><tspan sodipodi:role="line" x="105.32493" y="169.25755" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan850">NSM</tspan></text> </g> <g transform="translate(-170.13191,80.919145)" inkscape:label="jackpatch" id="jackpatch"> <title id="title890">jackpatch</title> <rect style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect892" width="67.73333" height="67.73333" x="71.133331" y="114.63333" /> <text id="text896" y="165.8237" x="106.38719" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" xml:space="preserve"><tspan id="tspan894" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" y="165.8237" x="106.38719" sodipodi:role="line">JP</tspan></text> </g> <g id="proxy" inkscape:label="proxy"> <title id="title940">proxy</title> <rect y="37.223072" x="-99.065941" height="67.73333" width="67.73333" id="rect902" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="-65.639175" y="93.759422" id="text906"><tspan sodipodi:role="line" x="-65.639175" y="93.759422" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan904">PRX</tspan></text> </g> <path d="m 231.64549,37.223072 h 67.73333 V 104.9564 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1679" /> <g style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="text1683" aria-label="PRX"> <path id="path1690" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 238.98532,48.420053 0.0677,45.339369 h 4.87229 V 72.578493 l 0.27069,-0.06767 h 4.60161 c 1.42108,0 2.63915,-0.406024 3.65421,-1.150401 1.01506,-0.744378 1.48876,-1.962451 1.48876,-3.654218 V 53.427685 c 0,-1.96245 -0.4737,-3.315864 -1.42109,-3.992571 -0.94739,-0.676707 -2.16546,-1.015061 -3.72188,-1.015061 z m 4.93996,4.872291 h 3.31587 c 0.47369,0 0.81205,0.135341 1.1504,0.473695 0.33835,0.338353 0.4737,0.744377 0.4737,1.218072 v 10.962654 c 0,0.473694 -0.13535,0.812048 -0.4737,1.150401 -0.33835,0.338354 -0.67671,0.473695 -1.1504,0.473695 h -3.31587 z" /> <path id="path1692" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 262.66997,53.292344 h 3.31586 c 0.4737,0 0.81205,0.135341 1.15041,0.473695 0.33835,0.338353 0.47369,0.744377 0.47369,1.218072 v 10.962654 c 0,0.473694 -0.13534,0.812048 -0.47369,1.150401 -0.33836,0.338354 -0.67671,0.473695 -1.15041,0.473695 h -3.31586 z m -4.93996,-4.872291 0.0677,45.339369 H 262.67 v -21.2486 h 1.21807 l 4.19559,21.2486 h 4.87229 L 268.69266,72.24014 c 0.60904,-0.203012 1.42109,-0.609037 2.50382,-1.218073 0.94739,-0.609036 1.48875,-1.759438 1.48875,-3.451206 V 53.427685 c 0,-1.285743 -0.47369,-2.436145 -1.48875,-3.518876 -1.01506,-1.015061 -2.16547,-1.488756 -3.51888,-1.488756 z" /> <path id="path1694" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 275.6627,48.420053 5.0753,23.346392 -4.80462,21.789965 h 5.00763 l 2.90984,-13.53414 2.90984,13.53414 h 4.93996 l -4.73695,-21.789965 5.07531,-23.346392 h -5.14298 l -3.04518,14.752213 -3.04518,-14.752213 z" /> </g> <path d="m 230.75774,195.55247 h 67.73333 v 67.73333 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1700" /> <g style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="text1704" aria-label="JP"> <path id="path1712" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 259.34614,252.35951 v -50.95604 h -5.0753 v 49.46729 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -1.75944 v 4.93996 h 3.04518 c 3.58655,0 5.41366,-1.69176 5.41366,-5.0753 z" /> <path id="path1714" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 263.40627,201.40347 0.0677,45.33937 h 4.87229 v -21.18093 l 0.27068,-0.0677 h 4.60161 c 1.42108,0 2.63916,-0.40602 3.65422,-1.1504 1.01506,-0.74438 1.48875,-1.96245 1.48875,-3.65422 v -14.27851 c 0,-1.96245 -0.47369,-3.31587 -1.42108,-3.99257 -0.94739,-0.67671 -2.16546,-1.01507 -3.72189,-1.01507 z m 4.93996,4.87229 h 3.31586 c 0.4737,0 0.81205,0.13535 1.1504,0.4737 0.33836,0.33835 0.4737,0.74438 0.4737,1.21807 v 10.96266 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -3.31586 z" /> </g> </g> </svg> 07070100000042000081A40000000000000000000000016258A65C0000ABA9000000000000000000000000000000000000004600000000new-session-manager-1.6.0+git.20220415.0f6719c/src/nsm-legacy-gui.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #include "Endpoint.hpp" #include <FL/Fl.H> #include <FL/Fl_Window.H> #include <FL/Fl_Double_Window.H> #include <FL/Fl_Widget.H> #include <FL/Fl.H> #include <FL/Fl_File_Chooser.H> #include <FL/Fl_Box.H> #include <FL/Fl_Pack.H> #include <FL/Fl_File_Chooser.H> #include <FL/Fl_Progress.H> #include "debug.h" #include <FL/Fl_Browser.H> #include <FL/Fl_Select_Browser.H> #include <FL/Fl_Tree.H> #include <FL/Fl_Hold_Browser.H> #include <FL/Fl_Tile.H> #include <FL/Fl_Shared_Image.H> #include <FL/Fl_Box.H> #include <FL/Fl_Text_Display.H> #include "FL/Fl_Packscroller.H" #include "FL/Fl_Scalepack.H" #include <unistd.h> #include <errno.h> #include <time.h> #include <getopt.h> #define APP_NAME "NSM Legacy GUI" #define APP_TITLE "NSM Legacy GUI" #pragma GCC diagnostic ignored "-Wunused-result" // static lo_address nsm_addr = NULL; static time_t last_ping_response; static OSC::Endpoint *osc; struct Daemon { const char *url; lo_address addr; bool is_child; Daemon ( ) { url = NULL; addr = NULL; is_child = false; } }; static std::list<Daemon*> daemon_list; /* list of all connected daemons */ #define foreach_daemon( _it ) for ( std::list<Daemon*>::iterator _it = daemon_list.begin(); _it != daemon_list.end(); ++ _it ) static Fl_Image * get_program_icon ( const char *name ) { const char *tries[] = { "/usr/local/share/icons/hicolor/32x32/apps/%s.png", "/usr/local/share/pixmaps/%s.png", "/usr/local/share/pixmaps/%s.xpm", "/usr/share/icons/hicolor/32x32/apps/%s.png", "/usr/share/icons/hicolor/128x128/apps/%s.png", "/usr/share/pixmaps/%s.png", "/usr/share/pixmaps/%s.xpm", }; for ( unsigned int i = 0; i < 6; i++ ) { char *icon_p; asprintf( &icon_p, tries[i], name ); Fl_Image *img = Fl_Shared_Image::get( icon_p, 32, 32 ); free( icon_p ); if ( img ) return img; } return NULL; } class NSM_Client : public Fl_Group { char *_client_id; char *_client_label; char *_client_name; Fl_Box *client_name; Fl_Box *icon_box; Fl_Progress *_progress; Fl_Light_Button *_dirty; Fl_Light_Button *_gui; Fl_Button *_remove_button; Fl_Button *_restart_button; Fl_Button *_kill_button; void set_label ( void ) { char *l; if ( _client_label && _client_label[0] != '\0' ) asprintf( &l, "%s (%s)", _client_name, _client_label ); else l = strdup( _client_name ); if ( ! icon_box->image() ) { Fl_Image *img = get_program_icon( _client_name ); if ( img ) { icon_box->image( img ); } } client_name->copy_label( l ); free(l); redraw(); } public: void name ( const char *v ) { if ( _client_name ) free( _client_name ); _client_name = strdup( v ); set_label(); } void client_label ( const char *s ) { if ( _client_label ) free( _client_label ); _client_label = strdup( s ); set_label(); } void client_id ( const char *v ) { if ( _client_id ) free( _client_id ); _client_id = strdup( v ); } void progress ( float f ) { _progress->value( f ); _progress->redraw(); } void dirty ( bool b ) { _dirty->value( b ); _dirty->redraw(); } void gui_visible ( bool b ) { _gui->value( b ); _gui->redraw(); } void has_optional_gui ( void ) { _gui->show(); _gui->redraw(); } void stopped ( bool b ) { if ( b ) { _remove_button->show(); _restart_button->show(); _kill_button->hide(); _gui->deactivate(); _dirty->deactivate(); redraw(); } else { _gui->activate(); _dirty->activate(); _kill_button->show(); _restart_button->hide(); _remove_button->hide(); } /* _restart_button->redraw(); */ /* _remove_button->redraw(); */ } void pending_command ( const char *command ) { _progress->copy_label( command ); stopped( 0 ); if ( ! strcmp( command, "ready" ) ) { _progress->value( 0.0f ); } else if ( ! strcmp( command, "quit" ) || ! strcmp( command, "kill" ) || ! strcmp( command, "error" ) ) { //Set a border color to indicate warning color( fl_color_average( FL_BLACK, FL_RED, 0.50 ) ); } else if ( ! strcmp( command, "stopped" ) ) { stopped( 1 ); } redraw(); } static void cb_button ( Fl_Widget *o, void * v ) { ((NSM_Client*)v)->cb_button( o ); } void cb_button ( Fl_Widget *o ) { if ( o == _dirty ) { MESSAGE( "Sending save."); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/gui/client/save", _client_id ); } } else if ( o == _gui ) { MESSAGE( "Sending hide/show GUI."); foreach_daemon ( d ) { if ( !_gui->value() ) osc->send( (*d)->addr, "/nsm/gui/client/show_optional_gui", _client_id ); else osc->send( (*d)->addr, "/nsm/gui/client/hide_optional_gui", _client_id ); } } else if ( o == _remove_button ) { MESSAGE( "Sending remove."); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/gui/client/remove", _client_id ); } } else if ( o == _restart_button ) { MESSAGE( "Sending resume" ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/gui/client/resume", _client_id ); } } else if ( o == _kill_button ) { MESSAGE( "Sending stop" ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/gui/client/stop", _client_id ); } } } const char * client_id ( void ) { return _client_id; } NSM_Client ( int X, int Y, int W, int H, const char *L ) : Fl_Group( X, Y, W, H, L ) { _client_id = NULL; _client_name = NULL; _client_label = NULL; align( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ); box( FL_UP_FRAME ); int yy = Y + H * 0.25; int hh = H * 0.50; int xx = X + W - ( 75 + Fl::box_dw( box() ) ); int ss = 2; /* /\* dummy group *\/ */ /* { Fl_Group *o = new Fl_Group( X, Y, W, H ); */ /* o->end(); */ /* resizable( o ); */ /* } */ { Fl_Pack *o = new Fl_Pack( X + 15, Y, 300 - 5, H ); o->type( FL_HORIZONTAL ); o->spacing( 10 ); { icon_box = new Fl_Box( 0, 0, 32, 32 ); } { Fl_Box *o = client_name = new Fl_Box( 0, 0, 300, 48 ); o->align( FL_ALIGN_INSIDE | FL_ALIGN_LEFT ); o->labeltype( FL_NORMAL_LABEL ); } o->end(); } { Fl_Box *o = new Fl_Box( X + 300, Y, 100, h() ); Fl_Group::current()->resizable(o); } { Fl_Progress *o = _progress = new Fl_Progress( xx, Y + H * 0.25, 75, H * 0.50, NULL ); o->box( FL_FLAT_BOX ); o->copy_label( "launch" ); o->labelsize( 12 ); o->minimum( 0.0f ); o->maximum( 1.0f ); } { Fl_Group *o = new Fl_Group( X + W - 400, Y, 400, H ); xx -= 50 + ss; { Fl_Light_Button *o = _dirty = new Fl_Light_Button( xx, yy, 50, hh, "SAVE" ); o->align( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ); o->labelsize( 9 ); o->box( FL_UP_BOX ); o->type(0); o->value( 0 ); o->callback( cb_button, this ); } xx -= 40 + ss; { Fl_Light_Button *o = _gui = new Fl_Light_Button( xx, yy, 40, hh, "GUI" ); o->align( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ); o->labelsize( 9 ); o->box( FL_UP_BOX ); o->type(0); o->value( 0 ); o->hide(); o->callback( cb_button, this ); } xx -= 25 + ss; { Fl_Button *o = _kill_button = new Fl_Button( xx, yy, 25, hh, "@square" ); o->labelsize( 9 ); o->box( FL_UP_BOX ); o->type(0); o->value( 0 ); o->tooltip( "Stop" ); o->callback( cb_button, this ); } xx -= 25 + ss; { Fl_Button *o = _restart_button = new Fl_Button( xx, yy, 25, hh ); o->box( FL_UP_BOX ); o->type(0); o->value( 0 ); o->label( "@>" ); o->tooltip( "Resume" ); o->hide(); o->callback( cb_button, this ); } xx -= 25 + ss; { Fl_Button *o = _remove_button = new Fl_Button( xx, yy, 25, hh ); o->box( FL_UP_BOX ); o->type(0); o->value( 0 ); o->label( "X" ); o->tooltip( "Remove" ); o->hide(); o->callback( cb_button, this ); } o->end(); } end(); } ~NSM_Client ( ) { if ( _client_id ) { free( _client_id ); _client_id = NULL; } if ( _client_name ) { free( _client_name ); _client_name = NULL; } if ( _client_label ) { free( _client_label ); _client_label = NULL; } if ( label() ) { free( (char*)label() ); label( NULL ); } } }; static void fl_awake_alert( void *v ) { if ( v ) { fl_alert( "%s", (char*)v ); free( v ); } } void browser_callback ( Fl_Widget *w, void * ) { w->window()->hide(); } class NSM_Controller : public Fl_Group { Fl_Text_Display *status_display; public: Fl_Pack *clients_pack; Fl_Pack *buttons_pack; Fl_Button *close_button; Fl_Button *abort_button; Fl_Button *save_button; Fl_Button *open_button; Fl_Button *new_button; Fl_Button *add_button; Fl_Button *duplicate_button; Fl_Button *quit_button; Fl_Button *refresh_button; Fl_Box *session_name_box; Fl_Tree *session_browser; int status_lines; static void cb_handle ( Fl_Widget *w, void *v ) { ((NSM_Controller*)v)->cb_handle( w ); } void log_status ( const char *s ) { time_t now; now = time( NULL ); struct tm * tm = localtime( &now ); char *ts; asprintf( &ts, "%02i:%02i:%02i ", tm->tm_hour, tm->tm_min, tm->tm_sec ); status_display->buffer()->append( ts ); free( ts ); status_display->buffer()->append( s ); status_display->scroll( ++status_lines, 0 ); status_display->buffer()->append( "\n" ); } void cb_handle ( Fl_Widget *w ) { if ( w == abort_button ) { if ( 0 == fl_choice( "Are you sure you want to close this session? Unsaved changes will be lost.", "Close anyway", "Cancel", NULL ) ) { MESSAGE( "Sending abort." ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/abort" ); } } } if ( w == close_button ) { MESSAGE( "Sending close." ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/close" ); } } else if ( w == save_button ) { MESSAGE( "Sending save." ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/save" ); } } else if ( w == open_button ) { const char *name = fl_input( "Open Session", NULL ); if ( ! name || name[0] == '\0' ) return; Fl_Tree_Item *item = session_browser->find_item( name ); if ( item ) session_browser->select_only( item, 1 ); } else if ( w == duplicate_button ) { const char *name = fl_input( "New Session", NULL ); if ( ! name || name[0] == '\0' ) return; MESSAGE( "Sending duplicate for: %s", name ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/duplicate", name ); } } else if ( w == quit_button ) { window()->do_callback( window(), this ); } else if ( w == refresh_button ) { session_browser->clear(); session_browser->redraw(); MESSAGE( "Refreshing session list." ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/list" ); } } else if ( w == session_browser ) { if ( session_browser->callback_reason() != FL_TREE_REASON_SELECTED ) return; Fl_Tree_Item *item = session_browser->callback_item(); if ( item ) session_browser->deselect( item, 0 ); //Deselect on program start, otherwise it looks like the first session is already loaded. if ( item->children() ) return; char name[1024]; session_browser->item_pathname( name, sizeof(name), item ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/open", name ); } } else if ( w == new_button ) { const char *name = fl_input( "New Session", NULL ); if ( ! name || name[0] == '\0' ) return; MESSAGE( "Sending new for: %s", name ); foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/new", name ); } } else if ( w == add_button ) { Fl_Select_Browser *browser; if ( daemon_list.size() > 1 ) { Fl_Window* win = new Fl_Window( window()->x(), window()->y(), 300, 400, "Choose Server" ); { { Fl_Box *o = new Fl_Box( 0,0, 300, 100 ); o->label( "Connected to multiple NSM servers, please select which one to add a client to." ); o->align( FL_ALIGN_CENTER | FL_ALIGN_INSIDE | FL_ALIGN_WRAP ); } { Fl_Select_Browser *o = browser = new Fl_Select_Browser( 0, 100, 300, 300 ); o->box( FL_ROUNDED_BOX ); o->callback( browser_callback, win ); foreach_daemon( d ) { o->add( (*d)->url ); } } } win->end(); win->show(); while ( win->visible() ) { Fl::wait(); } if ( ! browser->value() ) return; const char *n = fl_input( "Enter executable name" ); if ( !n || !*n || n[0] == '\0' ) return; char *name = strdup( n ); lo_address nsm_addr = lo_address_new_from_url( browser->text( browser->value() ) ); osc->send( nsm_addr, "/nsm/server/add", name ); free( name ); delete win; } else { const char *n = fl_input( "Enter executable name" ); if ( !n || !*n || n[0] == '\0' ) return; char *name = strdup( n ); MESSAGE( "Sending add for: %s", name ); /* FIXME: user should get to choose which system to do the add on */ foreach_daemon ( d ) { osc->send( (*d)->addr, "/nsm/server/add", name ); } free( name ); } } } NSM_Client * client_by_id ( const char *id ) { for ( int i = clients_pack->children(); i--; ) { NSM_Client *c = (NSM_Client*)clients_pack->child( i ); if ( ! strcmp( c->client_id(), id ) ) { return c; } } return NULL; } const char *session_name ( void ) const { return session_name_box->label(); } void session_name ( const char *name ) { session_name_box->copy_label( name ); if ( strlen( name ) ) { save_button->activate(); add_button->activate(); duplicate_button->activate(); abort_button->activate(); close_button->activate(); } else { save_button->deactivate(); add_button->deactivate(); duplicate_button->deactivate(); abort_button->deactivate(); close_button->deactivate(); } redraw(); } void client_stopped ( const char *client_id ) { NSM_Client *c = client_by_id( client_id ); if ( c ) { c->stopped( 1 ); } } void client_quit ( const char *client_id ) { NSM_Client *c = client_by_id( client_id ); if ( c ) { clients_pack->remove( c ); delete c; } if ( clients_pack->children() == 0 ) { ((Fl_Packscroller*)clients_pack->parent())->yposition( 0 ); } parent()->redraw(); } void client_new ( const char *client_id, const char *client_name ) { NSM_Client *c; c = client_by_id( client_id ); if ( c ) { c->name( client_name ); return; } c = new NSM_Client( 0, 0, w(), 40, NULL ); c->name( client_name ); c->client_id( client_id ); c->stopped( 0 ); clients_pack->add( c ); redraw(); } void client_pending_command ( NSM_Client *c, const char *command ) { if ( c ) { if ( ! strcmp( command, "removed" ) ) { clients_pack->remove( c ); delete c; parent()->redraw(); } else c->pending_command( command ); } } void add_session_to_list ( const char *name ) { session_browser->add( name ); session_browser->redraw(); } NSM_Controller ( int X, int Y, int W, int H, const char *L ) : Fl_Group( X, Y, W, H, L ) { status_lines = 0; align( FL_ALIGN_RIGHT | FL_ALIGN_CENTER | FL_ALIGN_INSIDE ); { Fl_Pack *o = buttons_pack = new Fl_Pack( X, Y, W, 30 ); o->type( Fl_Pack::HORIZONTAL ); o->box( FL_NO_BOX ); { Fl_Button *o = quit_button = new Fl_Button( 0, 0, 50, 50, "&Quit" ); o->shortcut( FL_CTRL | 'q' ); o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } { Fl_Button *o = refresh_button = new Fl_Button( 0, 0, 70, 50, "&Refresh" ); o->shortcut( FL_CTRL | 'r' ); o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } { Fl_Button *o = new_button = new Fl_Button( 0, 0, 100, 50, "&New Session" ); o->shortcut( FL_CTRL | 'n' ); o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } { Fl_Button *o = save_button = new Fl_Button( 0, 0, 105, 50, "&Save" ); o->shortcut( FL_CTRL | 's' ); o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } { Fl_Button *o = close_button = new Fl_Button( 0, 0, 105, 50, "Save && Close" ); o->shortcut( FL_CTRL | 'e' ); // is this a good key? o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } { Fl_Button *o = duplicate_button = new Fl_Button( 0, 0, 105, 50, "Save && &Dupl." ); o->shortcut( FL_CTRL | 'd' ); o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } { Fl_Button *o = open_button = new Fl_Button( 0, 0, 105, 50, "Save && &Open" ); o->shortcut( FL_CTRL | 'o' ); o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } { Fl_Button *o = abort_button = new Fl_Button( 0, 0, 160, 50, "Close &without Saving" ); o->shortcut( FL_CTRL | 'w' ); o->box( FL_UP_BOX ); o->callback( cb_handle, (void*)this ); } o->end(); } int SH = 14; { Fl_Tile *o = new Fl_Tile( X, Y + 30, W, H - 30 ); { Fl_Scalepack *o = new Fl_Scalepack( X, Y + 30, 300, H - ( 30 + SH ) ); o->type( FL_VERTICAL ); o->spacing( 2 ); { new Fl_Box( 0,0,100, 24, "Sessions" ); } { Fl_Tree *o = session_browser = new Fl_Tree( X, Y + 50, W / 3, H - ( 50 + SH ) ); o->callback( cb_handle, (void *)this ); o->sortorder( FL_TREE_SORT_ASCENDING ); o->showroot( 0 ); o->selectbox( FL_UP_FRAME ); o->box( FL_FLAT_BOX ); /* o->label( "Sessions" ); */ o->end(); Fl_Group::current()->resizable( o ); } // Fl_Tree o->end(); } Fl_Scalepack *scalepack; { Fl_Scalepack *o = scalepack = new Fl_Scalepack( X + 300, Y + 30, W - 300, H - ( 30 + SH ) ); o->type( FL_VERTICAL ); o->spacing( 2 ); { session_name_box = new Fl_Box( 0, 0, 100, 25, "" ); session_name_box->labelsize( 20 ); } { Fl_Button *o = add_button = new Fl_Button( 0, 0, 100, 25, "&Add Client to Session" ); o->shortcut( FL_CTRL | 'a' ); o->box( FL_UP_BOX ); o->align( FL_ALIGN_CLIP ); o->callback( cb_handle, (void*)this ); } { Fl_Packscroller *o = new Fl_Packscroller( 0, 0, 100, H - ( 30 + SH ) ); o->align( FL_ALIGN_TOP ); o->labeltype( FL_SHADOW_LABEL ); { Fl_Pack *o = clients_pack = new Fl_Pack( 0, 0, 100, 100 ); o->align( FL_ALIGN_TOP ); o->spacing( 4 ); o->type( Fl_Pack::VERTICAL ); o->end(); } o->end(); Fl_Group::current()->resizable( o ); } // Fl_Packscroller o->end(); /* Fl_Group::current()->resizable( o ); */ } // Fl_Scalepack { new Fl_Box( X + 1000, Y + 30, 100, H - ( 30 + SH )); } { Fl_Text_Display *o = status_display = new Fl_Text_Display( X, Y + H - SH, W, SH ); o->box( FL_UP_BOX ); Fl_Text_Buffer *b = new Fl_Text_Buffer(); o->textfont( FL_COURIER ); // Create the "technical log" look&feel o->textsize( 12 ); o->buffer(b); } o->end(); resizable( o ); } // Fl_tile end(); deactivate(); } int min_h ( void ) { return 500; } void ping ( void ) { if ( daemon_list.size() ) { foreach_daemon( d ) { osc->send( (*d)->addr, "/osc/ping" ); } } if ( last_ping_response ) { if ( time(NULL) - last_ping_response > 10 ) { if ( active() ) { deactivate(); log_status( "Server is not responding..." ); } } else { if ( !active() ) { log_status( "Server is back." ); activate(); } } } } int init_osc ( void ) { osc = new OSC::Endpoint(); if ( int r = osc->init( LO_UDP ) ) return r; osc->owner = this; osc->add_method( "/error", "sis", osc_handler, osc, "msg" ); osc->add_method( "/reply", "ss", osc_handler, osc, "msg" ); osc->add_method( "/reply", "s", osc_handler, osc, "" ); osc->add_method( "/nsm/server/broadcast", NULL, osc_broadcast_handler, osc, "msg" ); osc->add_method( "/nsm/gui/server_announce", "s", osc_handler, osc, "msg" ); osc->add_method( "/nsm/gui/server/message", "s", osc_handler, osc, "msg" ); osc->add_method( "/nsm/gui/gui_announce", "s", osc_handler, osc, "msg" ); osc->add_method( "/nsm/gui/session/session", "s", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/session/name", "ss", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/new", "ss", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/status", "ss", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/switch", "ss", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/progress", "sf", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/dirty", "si", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/has_optional_gui", "s", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/gui_visible", "si", osc_handler, osc, "path,display_name" ); osc->add_method( "/nsm/gui/client/label", "ss", osc_handler, osc, "path,display_name" ); osc->start(); return 0; } void announce ( const char *nsm_url ) { /* Daemon *d = new Daemon; */ /* d->url = nsm_url; */ lo_address nsm_addr = lo_address_new_from_url( nsm_url ); // d->is_child = true; /* daemon_list.push_back( d ); */ osc->send( nsm_addr, "/nsm/gui/gui_announce" ); } private: static int osc_broadcast_handler ( const char *path, const char *, lo_arg **, int argc, lo_message msg, void * ) { if ( ! argc ) /* need at least one argument... */ return 0; MESSAGE( "Relaying broadcast" ); foreach_daemon( d ) { char *u1 = lo_address_get_url( (*d)->addr ); char *u2 = lo_address_get_url( lo_message_get_source( msg ) ); if ( strcmp( u1, u2 ) ) { osc->send( (*d)->addr, path, msg ); } free( u1 ); free( u2 ); } return 0; } static int osc_handler ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { // OSC_DMSG(); NSM_Controller *controller = (NSM_Controller*)((OSC::Endpoint*)user_data)->owner; Fl::lock(); if ( !strcmp( path, "/nsm/gui/server/message" ) && !strcmp( types, "s" ) ) { controller->log_status( &argv[0]->s ); } else if ( !strcmp( path, "/nsm/gui/session/session" ) && ! strcmp( types, "s" ) ) { controller->add_session_to_list( &argv[0]->s ); } else if ( !strcmp( path, "/nsm/gui/gui_announce" ) ) { /* pre-existing server is replying to our announce message */ controller->activate(); lo_address nsm_addr = lo_message_get_source( msg ); osc->send( nsm_addr, "/nsm/server/list" ); } else if ( !strcmp( path, "/nsm/gui/server_announce" ) ) { /* must be a server we launched */ controller->activate(); Daemon *d = new Daemon; d->url = lo_address_get_url( lo_message_get_source( msg ) ); d->addr = lo_address_new_from_url( d->url ); d->is_child = true; daemon_list.push_back( d ); osc->send( d->addr, "/nsm/server/list" ); } else if ( !strcmp( path, "/nsm/gui/session/name" ) && !strcmp( types, "ss" )) { controller->session_name( &argv[0]->s ); if ( !strcmp( &argv[0]->s, "" ) ) { controller->session_browser->deselect_all(); } else { Fl_Tree_Item *o = controller->session_browser->find_item( &argv[1]->s ); if ( o ) { controller->session_browser->select_only( o, 0 ); controller->session_browser->show_item( o, 0 ); } } } else if (!strcmp( path, "/error" ) && !strcmp( types, "sis" ) ) { int err = argv[1]->i; if ( err != 0 ) { char *s; asprintf( &s, "Command %s failed with:\n\n%s", &argv[0]->s, &argv[2]->s ); Fl::awake(fl_awake_alert, s); } } else if (!strcmp( path, "/reply" ) && argc && 's' == *types ) { if ( !strcmp( &argv[0]->s, "/nsm/server/list" ) ) { controller->add_session_to_list( &argv[1]->s ); } else if ( !strcmp( &argv[0]->s, "/osc/ping" ) ) { last_ping_response = time( NULL ); } else if ( ! strcmp( types, "ss" ) ) { MESSAGE( "%s says %s", &argv[0]->s, &argv[1]->s); controller->log_status( &argv[1]->s ); } } if ( !strncmp( path, "/nsm/gui/client/", strlen( "/nsm/gui/client/" ) ) ) { if ( !strcmp( path, "/nsm/gui/client/new" ) && !strcmp( types, "ss" ) ) { controller->client_new( &argv[0]->s, &argv[1]->s ); } else { NSM_Client *c = controller->client_by_id( &argv[0]->s ); if ( c ) { if ( !strcmp( path, "/nsm/gui/client/status" ) && !strcmp( types, "ss" )) { controller->client_pending_command( c, &argv[1]->s ); } else if ( !strcmp( path, "/nsm/gui/client/progress" ) && !strcmp( types, "sf" )) { c->progress( argv[1]->f ); } else if ( !strcmp( path, "/nsm/gui/client/dirty" ) && !strcmp( types, "si" )) { c->dirty( argv[1]->i ); } else if ( !strcmp( path, "/nsm/gui/client/gui_visible" ) && !strcmp( types, "si" )) { c->gui_visible( argv[1]->i ); } else if ( !strcmp( path, "/nsm/gui/client/label" ) && !strcmp( types, "ss" )) { c->client_label( &argv[1]->s ); } else if ( !strcmp( path, "/nsm/gui/client/has_optional_gui" ) && !strcmp( types, "s" )) { c->has_optional_gui(); } else if ( !strcmp( path, "/nsm/gui/client/switch" ) && !strcmp( types, "ss" )) { c->client_id( &argv[1]->s ); } } else MESSAGE( "Got message %s from unknown client", path ); } } Fl::unlock(); Fl::awake(); return 0; } }; static NSM_Controller *controller; void ping ( void * ) { controller->ping(); Fl::repeat_timeout( 1.0, ping, NULL ); } void cb_main ( Fl_Widget *, void * ) { if ( Fl::event_key() != FL_Escape ) { int children = 0; foreach_daemon ( d ) { if ( (*d)->is_child ) ++children; } if ( children ) { if ( strlen( controller->session_name() ) ) { fl_message( "%s", "You have to close the session before you can quit." ); return; } } while ( Fl::first_window() ) Fl::first_window()->hide(); } } int main (int argc, char **argv ) { Fl::scheme( "gtk+" ); Fl::visual(FL_DOUBLE|FL_INDEX); // FLKT Double_Window: "higly recommended […] put before the first show() of any window in your program" fl_register_images(); Fl::lock(); Fl_Double_Window *main_window; { Fl_Double_Window *o = main_window = new Fl_Double_Window( 800, 600, APP_TITLE ); { main_window->xclass( APP_NAME ); Fl_Widget *o = controller = new NSM_Controller( 0, 0, main_window->w(), main_window->h(), NULL ); controller->session_name( "" ); Fl_Group::current()->resizable(o); } o->end(); o->size_range( main_window->w(), controller->min_h(), 0, 0 ); o->callback( (Fl_Callback*)cb_main, main_window ); o->show( 0, NULL ); } //Setting colors only after main window creation. //We keep them all in once place instead of setting them in the widgets Fl::set_color( FL_BACKGROUND_COLOR, 37, 40, 45 ); //These are the colors used as backgrounds by almost all widgets and used to draw the edges of all the boxtypes. Fl::set_color( FL_BACKGROUND2_COLOR, 55, 61, 69 ); //This color is used as a background by Fl_Input and other text widgets. Fl::set_color( FL_FOREGROUND_COLOR, 223, 237, 255 ); Fl::set_color( FL_INACTIVE_COLOR, 255, 0, 0 ); // Not used Fl::set_color( FL_SELECTION_COLOR, 80, 84, 92 ); // e.g. the currently selected session Fl::reload_scheme(); controller->session_browser->item_labelfgcolor( fl_rgb_color (213, 227, 245 ) ); // a bit darker than foreground static struct option long_options[] = { { "nsm-url", required_argument, 0, 'n' }, { "help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; int option_index = 0; int c = 0; while ( ( c = getopt_long_only( argc, argv, "", long_options, &option_index ) ) != -1 ) { switch ( c ) { case 'n': { MESSAGE( "Adding %s to daemon list", optarg ); Daemon *d = new Daemon; d->url = optarg; d->addr = lo_address_new_from_url( optarg ); daemon_list.push_back( d ); break; } case 'h': //Print usage message according to POSIX.1-2017 const char *usage = "nsm-legacy-gui - FLTK GUI for the 'New Session Manager'\n\n" "Usage:\n" " nsm-legacy-gui\n" " nsm-legacy-gui --help\n" "\n" "Options:\n" " --help Show this screen\n" " --nsm-url url Connect to a running nsmd [Example: osc.udp://mycomputer.localdomain:38356/].\n" " -- Everything after -- will be given to nsmd as server options. See nsmd --help .\n" "\n" "For backwards compatibility this executable also exist as symlink 'non-session-manager'\n" "\n" ""; puts ( usage ); exit(0); break; } } const char *nsm_url = getenv( "NSM_URL" ); if ( nsm_url ) { MESSAGE( "Found NSM URL of \"%s\" in environment, attempting to connect.", nsm_url ); Daemon *d = new Daemon; d->url = nsm_url; d->addr = lo_address_new_from_url( nsm_url ); daemon_list.push_back( d ); } if ( controller->init_osc() ) FATAL( "Could not create OSC server" ); if ( daemon_list.size() ) { foreach_daemon ( d ) { controller->announce( (*d)->url ); } } else { /* start a new daemon... */ MESSAGE( "Starting daemon..." ); char *url = osc->url(); if ( ! fork() ) { /* pass non-option arguments on to daemon */ char *args[4 + argc - optind]; int i = 0; args[i++] = strdup("nsmd"); args[i++] = strdup("--gui-url"); args[i++] = url; for ( ; optind < argc; i++, optind++ ) { MESSAGE( "Passing argument: %s", argv[optind] ); args[i] = argv[optind]; } args[i] = 0; if ( -1 == execvp( "nsmd", args ) ) { FATAL( "Error starting process: %s", strerror( errno ) ); } } free(url); } Fl::add_timeout( 1.0, ping, NULL ); Fl::run(); foreach_daemon ( d ) { if ( (*d)->is_child ) { MESSAGE( "Telling server to quit" ); osc->send( (*d)->addr, "/nsm/server/quit" ); } } return 0; } 07070100000043000081A40000000000000000000000016258A65C00003A97000000000000000000000000000000000000004600000000new-session-manager-1.6.0+git.20220415.0f6719c/src/nsm-legacy-gui.svg<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="68.791656mm" height="68.791656mm" viewBox="0 0 68.791656 68.791656" version="1.1" id="svg8" sodipodi:docname="icons.svg" inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> <defs id="defs2" /> <sodipodi:namedview units="px" id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.98994949" inkscape:cx="-233.62958" inkscape:cy="14.02435" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" showgrid="false" inkscape:window-width="2558" inkscape:window-height="1398" inkscape:window-x="0" inkscape:window-y="20" inkscape:window-maximized="1" /> <metadata id="metadata5"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g transform="translate(-232.45202,-114.10416)" inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"> <g id="nsm" inkscape:label="nsm" transform="translate(-172.56864)"> <title id="title888">nsm</title> <rect y="114.63333" x="71.133331" height="67.73333" width="67.73333" id="rect10" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="105.32493" y="169.25755" id="text852"><tspan sodipodi:role="line" x="105.32493" y="169.25755" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan850">NSM</tspan></text> </g> <g transform="translate(-170.13191,80.919145)" inkscape:label="jackpatch" id="jackpatch"> <title id="title890">jackpatch</title> <rect style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect892" width="67.73333" height="67.73333" x="71.133331" y="114.63333" /> <text id="text896" y="165.8237" x="106.38719" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" xml:space="preserve"><tspan id="tspan894" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" y="165.8237" x="106.38719" sodipodi:role="line">JP</tspan></text> </g> <g id="proxy" inkscape:label="proxy"> <title id="title940">proxy</title> <rect y="37.223072" x="-99.065941" height="67.73333" width="67.73333" id="rect902" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="-65.639175" y="93.759422" id="text906"><tspan sodipodi:role="line" x="-65.639175" y="93.759422" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan904">PRX</tspan></text> </g> <path d="m 231.64549,37.223072 h 67.73333 V 104.9564 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1679" /> <g style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="text1683" aria-label="PRX"> <path id="path1690" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 238.98532,48.420053 0.0677,45.339369 h 4.87229 V 72.578493 l 0.27069,-0.06767 h 4.60161 c 1.42108,0 2.63915,-0.406024 3.65421,-1.150401 1.01506,-0.744378 1.48876,-1.962451 1.48876,-3.654218 V 53.427685 c 0,-1.96245 -0.4737,-3.315864 -1.42109,-3.992571 -0.94739,-0.676707 -2.16546,-1.015061 -3.72188,-1.015061 z m 4.93996,4.872291 h 3.31587 c 0.47369,0 0.81205,0.135341 1.1504,0.473695 0.33835,0.338353 0.4737,0.744377 0.4737,1.218072 v 10.962654 c 0,0.473694 -0.13535,0.812048 -0.4737,1.150401 -0.33835,0.338354 -0.67671,0.473695 -1.1504,0.473695 h -3.31587 z" /> <path id="path1692" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 262.66997,53.292344 h 3.31586 c 0.4737,0 0.81205,0.135341 1.15041,0.473695 0.33835,0.338353 0.47369,0.744377 0.47369,1.218072 v 10.962654 c 0,0.473694 -0.13534,0.812048 -0.47369,1.150401 -0.33836,0.338354 -0.67671,0.473695 -1.15041,0.473695 h -3.31586 z m -4.93996,-4.872291 0.0677,45.339369 H 262.67 v -21.2486 h 1.21807 l 4.19559,21.2486 h 4.87229 L 268.69266,72.24014 c 0.60904,-0.203012 1.42109,-0.609037 2.50382,-1.218073 0.94739,-0.609036 1.48875,-1.759438 1.48875,-3.451206 V 53.427685 c 0,-1.285743 -0.47369,-2.436145 -1.48875,-3.518876 -1.01506,-1.015061 -2.16547,-1.488756 -3.51888,-1.488756 z" /> <path id="path1694" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 275.6627,48.420053 5.0753,23.346392 -4.80462,21.789965 h 5.00763 l 2.90984,-13.53414 2.90984,13.53414 h 4.93996 l -4.73695,-21.789965 5.07531,-23.346392 h -5.14298 l -3.04518,14.752213 -3.04518,-14.752213 z" /> </g> <path d="m 230.75774,195.55247 h 67.73333 v 67.73333 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1700" /> <path d="m 256.70029,252.35951 v -50.95604 h -5.0753 v 49.46729 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -1.75944 v 4.93996 h 3.04518 c 3.58655,0 5.41366,-1.69176 5.41366,-5.0753 z" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;line-height:1.25;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="path1712" /> <path d="m 266.05212,201.40347 0.0677,45.33937 h 4.87229 v -21.18093 l 0.27068,-0.0677 h 4.60161 c 1.42108,0 2.63916,-0.40602 3.65422,-1.1504 1.01506,-0.74438 1.48875,-1.96245 1.48875,-3.65422 v -14.27851 c 0,-1.96245 -0.47369,-3.31587 -1.42108,-3.99257 -0.94739,-0.67671 -2.16546,-1.01507 -3.72189,-1.01507 z m 4.93996,4.87229 h 3.31586 c 0.4737,0 0.81205,0.13535 1.1504,0.4737 0.33836,0.33835 0.4737,0.74438 0.4737,1.21807 v 10.96266 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -3.31586 z" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;line-height:1.25;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="path1714" /> <path d="m 232.98119,114.63333 h 67.73333 v 67.73333 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1875" /> <g style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="text1879" aria-label="NSM"> <path id="path1886" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 236.41654,123.6475 0.13535,0.27068 v 45.33937 h 5.00763 v -29.70743 l 8.79719,29.70743 h 5.34598 l -0.13534,-0.40602 V 123.6475 h -5.21064 v 29.63977 l -8.72952,-29.63977 z" /> <path id="path1888" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 269.64283,169.52824 c 1.35341,0 2.50382,-0.4737 3.45121,-1.35342 1.01506,-0.94739 1.48875,-2.09779 1.48875,-3.4512 v -14.48153 c 0,-1.28575 -0.47369,-2.43615 -1.35341,-3.45121 -0.94739,-0.94739 -2.16546,-1.6241 -3.58655,-2.09779 l -3.65422,-1.1504 c -0.40602,-0.13534 -0.81205,-0.4737 -1.1504,-0.94739 -0.33835,-0.54137 -0.47369,-1.01506 -0.47369,-1.48876 v -10.82731 c 0,-0.4737 0.13534,-0.81205 0.47369,-1.1504 0.33835,-0.33836 0.67671,-0.4737 1.1504,-0.4737 h 1.6241 c 0.47369,0 0.81205,0.13534 1.1504,0.4737 0.33836,0.33835 0.4737,0.6767 0.4737,1.1504 v 3.51888 h 5.00763 v -5.27832 c 0,-1.42108 -0.4737,-2.57148 -1.42109,-3.51887 -1.01506,-0.94739 -2.16546,-1.42109 -3.51887,-1.42109 h -4.93996 c -1.42109,0 -2.57149,0.4737 -3.51888,1.42109 -0.94739,0.94739 -1.42108,2.09779 -1.42108,3.51887 v 14.21085 c 0,1.28574 0.47369,2.50381 1.35341,3.51888 0.94739,1.01506 2.16546,1.69176 3.58655,2.16546 l 3.65421,1.08273 c 0.4737,0.13534 0.81205,0.40602 1.15041,0.87972 0.33835,0.47369 0.47369,0.87972 0.47369,1.35341 v 11.16567 c 0,0.47369 -0.13534,0.87972 -0.47369,1.21807 -0.33836,0.33835 -0.67671,0.47369 -1.15041,0.47369 h -1.62409 c -0.4737,0 -0.81205,-0.13534 -1.1504,-0.47369 -0.33836,-0.33835 -0.4737,-0.74438 -0.4737,-1.21807 v -3.51888 h -5.00763 v 5.34599 c 0,1.35341 0.47369,2.50381 1.42108,3.4512 1.01506,0.87972 2.16547,1.35342 3.51888,1.35342 z" /> <path id="path1890" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 278.30458,123.91818 0.13534,45.33937 h 5.14298 v -31.94057 l 4.19558,15.97029 h 0.67671 l 3.99257,-15.97029 v 31.94057 h 5.27831 v -45.33937 h -4.80462 l -4.80462,15.0229 -4.87229,-15.0229 z" /> </g> <text id="text1896" y="101.68526" x="-171.08479" style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583" xml:space="preserve"><tspan style="stroke-width:0.264583" y="101.68526" x="-171.08479" id="tspan1894" sodipodi:role="line">Texts and</tspan><tspan id="tspan1898" style="stroke-width:0.264583" y="114.91438" x="-171.08479" sodipodi:role="line">Objects</tspan></text> <text id="text1902" y="105.24335" x="349.29498" style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583" xml:space="preserve"><tspan style="stroke-width:0.264583" y="105.24335" x="349.29498" id="tspan1900" sodipodi:role="line">Converted</tspan><tspan id="tspan1904" style="stroke-width:0.264583" y="118.47247" x="349.29498" sodipodi:role="line">to paths</tspan></text> <text id="text1908" y="39.494576" x="41.722755" style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583" xml:space="preserve"><tspan style="stroke-width:0.264583" y="39.494576" x="41.722755" id="tspan1906" sodipodi:role="line">Select Icon, Ctrl+Shift+R</tspan><tspan id="tspan1910" style="stroke-width:0.264583" y="52.723701" x="41.722755" sodipodi:role="line">to resize to selection.</tspan><tspan id="tspan1912" style="stroke-width:0.264583" y="65.952827" x="41.722755" sodipodi:role="line">File -> Save Copy </tspan><tspan id="tspan1914" style="stroke-width:0.264583" y="79.181946" x="41.722755" sodipodi:role="line">Inkscape SVG works as icon</tspan></text> </g> </svg> 07070100000044000081A40000000000000000000000016258A65C0000316E000000000000000000000000000000000000004500000000new-session-manager-1.6.0+git.20220415.0f6719c/src/nsm-proxy-gui.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #pragma GCC diagnostic ignored "-Wunused-parameter" #define _MODULE_ "nsm-proxy-gui" #define APP_NAME "NSM Proxy" #define APP_TITLE "NSM Proxy" #include <FL/Fl_File_Chooser.H> #include <FL/Fl_Text_Display.H> #include "NSM_Proxy_UI.H" #include <lo/lo.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <getopt.h> lo_server losrv; lo_address nsmp_addr; static NSM_Proxy_UI *ui; static char *client_error; int osc_update ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { //Updates are arriving one OSC message at a time. printf( "Got update for %s\n", path ); Fl::lock(); if (!strcmp( path, "/nsm/proxy/label" )) ui->label_input->value( &argv[0]->s ); else if (!strcmp( path, "/nsm/proxy/arguments" )) ui->arguments_input->value( &argv[0]->s ); else if (!strcmp( path, "/nsm/proxy/executable" )) { ui->executable_input->value( &argv[0]->s ); if ( strcmp( &argv[0]->s , "") ) { //We want to avoid that the button is always labeled 'Start', creating the //false impression that a new sub-client instance is started each time you press it. //If the string is empty there is no program running at the moment. //This does not cover all cases but gets us there 90%, which is good enough for something cosmetic. ui->start_button->label("Ok"); } } else if (!strcmp( path, "/nsm/proxy/config_file" )) ui->config_file_input->value( &argv[0]->s ); else if (!strcmp( path, "/nsm/proxy/save_signal" )) { if ( argv[0]->i == SIGUSR1 ) ui->save_signal_choice->value( 1 ); else if ( argv[0]->i == SIGUSR2 ) ui->save_signal_choice->value( 2 ); else if ( argv[0]->i == SIGINT ) ui->save_signal_choice->value( 3 ); else ui->save_signal_choice->value( 0 ); } else if (!strcmp( path, "/nsm/proxy/stop_signal" )) { if ( argv[0]->i == SIGTERM ) ui->stop_signal_choice->value( 0 ); else if ( argv[0]->i == SIGINT ) ui->stop_signal_choice->value( 1 ); else if ( argv[0]->i == SIGHUP ) ui->stop_signal_choice->value( 2 ); } if (!strcmp( path, "/nsm/proxy/client_error" )) { if ( client_error != NULL ) free(client_error); client_error = NULL; if ( strlen(&argv[0]->s) > 0 ) client_error = strdup(&argv[0]->s); } Fl::unlock(); return 0; } void init_osc ( const char *osc_port ) { lo_server_thread loth = lo_server_thread_new( osc_port, NULL ); losrv = lo_server_thread_get_server( loth ); //error_handler ); char *url = lo_server_get_url(losrv); printf("OSC: %s\n",url); free(url); /* GUI */ lo_server_thread_add_method( loth, "/nsm/proxy/executable", "s", osc_update, NULL ); lo_server_thread_add_method( loth, "/nsm/proxy/arguments", "s", osc_update, NULL ); lo_server_thread_add_method( loth, "/nsm/proxy/config_file", "s", osc_update, NULL ); lo_server_thread_add_method( loth, "/nsm/proxy/label", "s", osc_update, NULL ); lo_server_thread_add_method( loth, "/nsm/proxy/save_signal", "i", osc_update, NULL ); lo_server_thread_add_method( loth, "/nsm/proxy/stop_signal", "i", osc_update, NULL ); lo_server_thread_add_method( loth, "/nsm/proxy/client_error", "s", osc_update, NULL ); lo_server_thread_start( loth ); } /*****************/ /* GUI Callbacks */ /*****************/ void handle_kill ( Fl_Widget *o, void *v ) { lo_send_from( nsmp_addr, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/kill", "" ); } void handle_start ( Fl_Widget *o, void *v ) { //Executed when Start Button is clicked. //"/nsm/proxy/start" for the sub-client is always sent, no matter if the software already runs. //nsm-proxy.cpp handles the redundancy. lo_send_from( nsmp_addr, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/start", "sss", ui->executable_input->value(), ui->arguments_input->value(), ui->config_file_input->value() ); } void handle_label ( Fl_Widget *o, void *v ) { lo_send_from( nsmp_addr, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/label", "s", ui->label_input->value() ); } void handle_executable ( Fl_Widget *o, void *v ) { //Autofill the label field when the executable name is edited ui->label_input->value( ui->executable_input->value() ); //This does not trigger handle_label so we call it manually lo_send_from( nsmp_addr, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/label", "s", ui->label_input->value() ); } void handle_config_file ( Fl_Widget *o, void *v ) { } void handle_config_file_browse ( Fl_Widget *o, void *v ) { const char * file = fl_file_chooser( "Pick file", "*", NULL, 1 ); ui->config_file_input->value( file ); } void handle_save_signal ( Fl_Widget *o, void *v ) { int sig = 0; const char* picked = ui->save_signal_choice->mvalue()->label(); if ( !strcmp( picked, "SIGUSR1" ) ) sig = SIGUSR1; else if ( !strcmp( picked, "SIGUSR2" ) ) sig = SIGUSR2; else if ( !strcmp( picked, "SIGINT" ) ) sig = SIGINT; lo_send_from( nsmp_addr, losrv, LO_TT_IMMEDIATE,"/nsm/proxy/save_signal", "i", sig ); } void handle_stop_signal ( Fl_Widget *o, void *v ) { int sig = SIGTERM; const char* picked = ui->stop_signal_choice->mvalue()->label(); if ( !strcmp( picked, "SIGTERM" ) ) sig = SIGTERM; else if ( !strcmp( picked, "SIGINT" ) ) sig = SIGINT; else if ( !strcmp( picked, "SIGHUP" ) ) sig = SIGHUP; lo_send_from( nsmp_addr, losrv, LO_TT_IMMEDIATE,"/nsm/proxy/stop_signal", "i", sig ); } void connect_ui ( void ) { ui->executable_input->callback( handle_executable, NULL ); ui->config_file_input->callback( handle_config_file, NULL ); ui->kill_button->callback( handle_kill, NULL ); ui->start_button->callback( handle_start, NULL ); ui->save_signal_choice->callback( handle_save_signal, NULL ); ui->stop_signal_choice->callback( handle_stop_signal, NULL ); ui->label_input->callback( handle_label, NULL ); ui->config_file_browse_button->callback( handle_config_file_browse, NULL ); } void cb_dismiss_button ( Fl_Widget *w, void *v ) { w->window()->hide(); } void check_error ( void *v ) { if ( client_error ) { { Fl_Double_Window *o = new Fl_Double_Window(600,300+15,"Abnormal Termination"); //Create a new floating Window that shows an error message. { Fl_Box *o = new Fl_Box(0+15,0+15,600-30,50); o->box(FL_BORDER_BOX); o->color(FL_RED); o->labelcolor(FL_WHITE); o->align(FL_ALIGN_CENTER|FL_ALIGN_WRAP); o->copy_label( client_error ); } { Fl_Text_Display *o = new Fl_Text_Display(0+15,50+15,600-30,300-75-30); o->buffer(new Fl_Text_Buffer()); o->buffer()->loadfile( "error.log" ); } { Fl_Button *o = new Fl_Button(600-75-15,300-25,75,25,"Dismiss"); o->callback(cb_dismiss_button,0); } o->show(); } free(client_error); client_error = NULL; } Fl::repeat_timeout( 0.5f, check_error, v ); } int main ( int argc, char **argv ) { //The NSM-Proxy GUI is a client that communicates with another binary, nsm-proxy via OSC. //This GUI executable is actually closed and restarted each time you show/hide the GUI. //In other words: it is not a persistent GUI state. //Command line parameters const char * gui_url = NULL; static struct option long_options[] = { { "connect-to", required_argument, 0, 'u' }, { "help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; int option_index = 0; int c = 0; while ( ( c = getopt_long_only( argc, argv, "", long_options, &option_index ) ) != -1 ) { switch ( c ) { case 'u': { gui_url = optarg; break; } case 'h': { const char *usage = "nsm-proxy-gui - GUI for nsm-proxy, a wrapper for executables without direct NSM-Support.\n\n" "Usage:\n" " nsm-proxy-gui --help\n" " nsm-proxy-gui --connect-to\n" "\n" "Options:\n" " --help Show this screen\n" " --connect-to Connect to running nsm-proxy\n" "\n\n" "nsmd-proxy-gui is usually not called by the user directly,\n" "but autostarted when nsm-proxy is added to a session (through a GUI).\n" ""; puts ( usage ); exit(0); break; } } } if ( gui_url == NULL ) exit(1); init_osc( NULL ); nsmp_addr = lo_address_new_from_url( gui_url ); if ( ! nsmp_addr ) exit(1); printf( "Connecting to nsm-proxy at: %s\n", gui_url ); ui = new NSM_Proxy_UI; Fl::scheme( "gtk+" ); Fl::visual(FL_DOUBLE|FL_INDEX); // FLKT Double_Window: "higly recommended […] put before the first show() of any window in your program" Fl_Double_Window *w = ui->make_window(); connect_ui(); lo_send_from( nsmp_addr, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/update", "" ); //The config file option allows the user to choose a different config file for the proxy settings. //This does more harm than good, so we hide the gui field. ui->config_file_input->hide(); w->show(); Fl::lock(); //Setting colors only after main window creation. //We keep them all in once place instead of setting them in the widgets //Colors are the same as nsm-legacy-gui.cpp . If one changes you need to change the other by hand. Fl::set_color( 55, 223, 237, 255 ); //Override FLUID palette with RGB Value. 55 is label text. Same as FL_FOREGROUND_COLOR Fl::set_color( 41, 55, 61, 69 ); //Override FLUID palette with RGB Value. 41 is label background Fl::set_color( FL_DARK1, 37, 40, 45 ); //Main window background Fl::set_color( FL_BACKGROUND_COLOR, 37, 40, 45 ); //These are the colors used as backgrounds by almost all widgets and used to draw the edges of all the boxtypes. Fl::set_color( FL_BACKGROUND2_COLOR, 55, 61, 69 ); //This color is used as a background by Fl_Input and other text widgets. Fl::set_color( FL_FOREGROUND_COLOR, 223, 237, 255 ); Fl::set_color( FL_INACTIVE_COLOR, 255, 0, 0 ); // Not used Fl::set_color( FL_SELECTION_COLOR, 80, 84, 92 ); // e.g. the currently selected session Fl::reload_scheme(); Fl::add_timeout( 0.5f, check_error, NULL ); Fl::run(); return 0; } 07070100000045000081A40000000000000000000000016258A65C0000512A000000000000000000000000000000000000004100000000new-session-manager-1.6.0+git.20220415.0f6719c/src/nsm-proxy.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wunused-result" #define _MODULE_ "nsm-proxy" #define APP_NAME "NSM Proxy" #define APP_TITLE "NSM Proxy" #include "debug.h" #include <lo/lo.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/signalfd.h> #include <sys/stat.h> #include <sys/wait.h> #include <getopt.h> static lo_server losrv; static lo_address nsm_addr; static lo_address gui_addr; static int nsm_is_active; static char *project_file; static int die_now = 0; static int signal_fd; static char *nsm_client_id; static char *nsm_display_name; #define CONFIG_FILE_NAME "nsm-proxy.config" void show_gui ( void ); class NSM_Proxy { char *_label; char *_executable; char *_config_file; char *_arguments; int _save_signal; int _stop_signal; int _pid; char *_client_error; public: int stop_signal ( void ) {return _stop_signal;} NSM_Proxy ( ) { _label = _executable = _arguments = _config_file = 0; _save_signal = 0; _stop_signal = SIGTERM; _pid = 0; _client_error = 0; } ~NSM_Proxy ( ) { } void handle_client_death ( int status ) { printf( "proxied process died unexpectedly... not dying\n" ); /* proxied process died unexpectedly */ if ( _client_error != NULL ) free(_client_error); asprintf(&_client_error, "The proxied process terminated abnormally during invocation. Exit status: %i.", status ); show_gui(); _pid = 0; } void kill ( void ) { if ( _pid ) { ::kill( _pid, _stop_signal ); } } bool start ( const char *executable, const char *arguments, const char *config_file ) { if ( _executable ) free( _executable ); if ( _arguments ) free( _arguments ); if ( _config_file ) free( _config_file ); _executable = strdup( executable ); if ( arguments ) _arguments = strdup( arguments ); else _arguments = NULL; if ( config_file ) _config_file = strdup( config_file ); else _config_file = NULL; return start(); } bool start ( void ) { dump( project_file ); if ( _pid ) /* already running */ return true; if ( !_executable ) { WARNING( "Executable is null." ); return false; } int pid; if ( ! (pid = fork()) ) { MESSAGE( "Launching %s\n", _executable ); // char *args[] = { strdup( executable ), NULL }; char *cmd; if ( _arguments ) asprintf( &cmd, "exec %s %s >error.log 2>&1", _executable, _arguments ); else asprintf( &cmd, "exec %s >error.log 2>&1", _executable ); char *args[] = { strdup("/bin/sh"), strdup( "-c" ), cmd, NULL }; setenv( "NSM_CLIENT_ID", nsm_client_id, 1 ); setenv( "NSM_SESSION_NAME", nsm_display_name, 1 ); if ( _config_file ) setenv( "CONFIG_FILE", _config_file, 1 ); unsetenv( "NSM_URL" ); if ( -1 == execvp( "/bin/sh", args ) ) { WARNING( "Error starting process: %s", strerror( errno ) ); exit(1); } } _pid = pid; return _pid > 0; } void save_signal ( int s ) { _save_signal = s; } void stop_signal ( int s ) { _stop_signal = s; } void label ( const char *s ) { if ( _label ) free( _label ); _label = strdup( s ); lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/label", "s", _label ); } void save ( void ) { MESSAGE( "Sending process save signal" ); if ( _pid ) ::kill( _pid, _save_signal ); } bool dump ( const char *path ) { //Dump current config to file. This happens quite often. char *fname; asprintf( &fname, "%s/%s", path, CONFIG_FILE_NAME ); FILE *fp = fopen( fname, "w" ); free( fname ); if ( !fp ) { WARNING( "Error opening file for saving: %s", strerror( errno ) ); return false; } if ( _executable && strlen(_executable) ) fprintf( fp, "executable\n\t%s\n", _executable ); if ( _arguments && strlen(_arguments) ) fprintf( fp, "arguments\n\t%s\n", _arguments ); if ( _config_file && strlen(_config_file) ) fprintf( fp, "config file\n\t%s\n", _config_file ); fprintf( fp, "save signal\n\t%i\n", _save_signal ); fprintf( fp, "stop signal\n\t%i\n", _stop_signal ); if ( _label && strlen(_label) ) fprintf( fp, "label\n\t%s\n", _label ); fclose( fp ); return true; } bool restore ( const char *path ) { FILE *fp = fopen( path, "r" ); if ( ! fp ) { WARNING( "Error opening file for restore: %s", strerror( errno ) ); return false; } char *name; char *value; MESSAGE( "Loading file config \"%s\"", path ); while ( 2 == fscanf( fp, "%m[^\n]\n\t%m[^\n]\n", &name, &value ) ) { MESSAGE( "%s=%s", name, value ); if ( !strcmp( name, "executable" ) ) _executable = value; else if (!strcmp( name, "arguments" ) ) _arguments = value; else if (!strcmp( name, "config file" ) ) _config_file = value; else if ( !strcmp( name, "save signal" ) ) { _save_signal = atoi( value ); free( value ); } else if ( !strcmp( name, "stop signal" ) ) { _stop_signal = atoi( value ); free( value ); } else if ( !strcmp( name, "label" ) ) { label( value ); free( value ); } else { WARNING( "Unknown option \"%s\" in config file", name ); } free( name ); } fclose( fp ); start(); return true; } void update ( lo_address to ) { //Each send triggers one osc_update in the Proxy-GUI. MESSAGE( "Sending update" ); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/save_signal", "i", _save_signal ); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/label", "s", _label ? _label : "" ); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/executable", "s", _executable ? _executable : "" ); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/arguments", "s", _arguments ? _arguments : "" ); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/config_file", "s", _config_file ? _config_file : "" ); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/stop_signal", "i", _stop_signal ); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/client_error", "s", _client_error ? _client_error : "" ); } }; NSM_Proxy *nsm_proxy; bool snapshot ( const char *file ) { return nsm_proxy->dump(file); } void announce ( const char *nsm_url, const char *client_name, const char *process_name ) { printf( "Announcing to NSM\n" ); lo_address to = lo_address_new_from_url( nsm_url ); int pid = (int)getpid(); lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/server/announce", "sssiii", client_name, ":optional-gui:", process_name, 1, /* api_major_version */ 0, /* api_minor_version */ pid ); lo_address_free( to ); } bool open ( const char *file ) { char *path; asprintf( &path, "%s/%s", file, CONFIG_FILE_NAME ); bool r = nsm_proxy->restore( path ); free( path ); return r; } /****************/ /* OSC HANDLERS */ /****************/ /* NSM */ int osc_announce_error ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { if ( strcmp( types, "sis" ) ) return -1; if ( strcmp( "/nsm/server/announce", &argv[0]->s ) ) return -1; printf( "Failed to register with NSM: %s\n", &argv[2]->s ); nsm_is_active = 0; return 0; } int osc_announce_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { if ( strcmp( "/nsm/server/announce", &argv[0]->s ) ) return -1; printf( "Successfully registered. NSM says: %s", &argv[1]->s ); nsm_is_active = 1; nsm_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) ) ); return 0; } int osc_save ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { bool r = snapshot( project_file ); nsm_proxy->save(); if ( r ) lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" ); else lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/error", "sis", path, -1, "Error saving project file" ); return 0; } static int gui_pid; void show_gui ( void ) { int pid; if ( ! (pid = fork()) ) { char executable[] = "nsm-proxy-gui"; MESSAGE( "Launching %s\n", executable ); char *url = lo_server_get_url( losrv ); char *args[] = { executable, strdup( "--connect-to" ), url, NULL }; if ( -1 == execvp( executable, args ) ) { WARNING( "Error starting process: %s", strerror( errno ) ); exit(1); } } gui_pid = pid; lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_shown", "" ); } int osc_show_gui ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { show_gui(); /* FIXME: detect errors */ lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" ); return 0; } void hide_gui ( void ) { if ( gui_pid ) { kill( gui_pid, SIGTERM ); } } int osc_hide_gui ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { hide_gui(); lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" ); /* FIXME: detect errors */ lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" ); return 0; } int osc_open ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { const char *new_path = &argv[0]->s; const char *display_name = &argv[1]->s; const char *client_id = &argv[2]->s; if ( nsm_client_id ) free(nsm_client_id); nsm_client_id = strdup( client_id ); if ( nsm_display_name ) free( nsm_display_name ); nsm_display_name = strdup( display_name ); char *new_filename; mkdir( new_path, 0777 ); chdir( new_path ); asprintf( &new_filename, "%s/%s", new_path, CONFIG_FILE_NAME ); struct stat st; if ( 0 == stat( new_filename, &st ) ) { if ( open( new_path ) ) { } else { lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/error", "sis", path, -1, "Could not open file" ); return 0; } lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" ); } else { show_gui(); } if ( project_file ) free( project_file ); project_file = strdup( new_path ); // new_filename; lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" ); if ( gui_addr ) nsm_proxy->update( gui_addr ); return 0; } /* GUI */ int osc_label ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { nsm_proxy->label( &argv[0]->s ); return 0; } int osc_save_signal ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { nsm_proxy->save_signal( argv[0]->i ); return 0; } int osc_stop_signal ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { nsm_proxy->stop_signal( argv[0]->i ); return 0; } int osc_start ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { snapshot( project_file ); if ( nsm_proxy->start( &argv[0]->s, &argv[1]->s, &argv[2]->s ) ) { hide_gui(); } return 0; } int osc_kill ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { nsm_proxy->kill(); return 0; } int osc_update ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { lo_address to = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); nsm_proxy->update( to ); gui_addr = to; return 0; } void signal_handler ( int x ) { die_now = 1; } void set_traps ( void ) { signal( SIGHUP, signal_handler ); signal( SIGINT, signal_handler ); // signal( SIGQUIT, signal_handler ); // signal( SIGSEGV, signal_handler ); // signal( SIGPIPE, signal_handler ); signal( SIGTERM, signal_handler ); } void init_osc ( const char *osc_port ) { losrv = lo_server_new( osc_port, NULL ); //error_handler ); char *url = lo_server_get_url(losrv); printf("OSC: %s\n",url); free(url); /* NSM */ lo_server_add_method( losrv, "/nsm/client/save", "", osc_save, NULL ); lo_server_add_method( losrv, "/nsm/client/open", "sss", osc_open, NULL ); lo_server_add_method( losrv, "/nsm/client/show_optional_gui", "", osc_show_gui, NULL ); lo_server_add_method( losrv, "/nsm/client/hide_optional_gui", "", osc_hide_gui, NULL ); lo_server_add_method( losrv, "/error", "sis", osc_announce_error, NULL ); lo_server_add_method( losrv, "/reply", "ssss", osc_announce_reply, NULL ); /* GUI */ lo_server_add_method( losrv, "/nsm/proxy/label", "s", osc_label, NULL ); lo_server_add_method( losrv, "/nsm/proxy/save_signal", "i", osc_save_signal, NULL ); lo_server_add_method( losrv, "/nsm/proxy/stop_signal", "i", osc_stop_signal, NULL ); lo_server_add_method( losrv, "/nsm/proxy/kill", "", osc_kill, NULL ); lo_server_add_method( losrv, "/nsm/proxy/start", "sss", osc_start, NULL ); lo_server_add_method( losrv, "/nsm/proxy/update", "", osc_update, NULL ); } void die ( void ) { if ( gui_pid ) { MESSAGE( "Killing GUI" ); kill( gui_pid, SIGTERM ); } nsm_proxy->kill(); exit(0); } void handle_sigchld ( ) { for ( ;; ) { int status; pid_t pid = waitpid(-1, &status, WNOHANG); if (pid <= 0) break; if ( pid == gui_pid ) { lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" ); gui_pid = 0; /* don't care... */ continue; } if ( WIFSIGNALED(status) ) { /* process was killed via signal */ if (WTERMSIG(status) == SIGTERM || WTERMSIG(status) == SIGHUP || WTERMSIG(status) == SIGINT || WTERMSIG(status) == SIGKILL ) { /* process was killed via an appropriate signal */ MESSAGE( "child was killed (maybe by us)\n" ); die_now = 1; continue; } } else if ( WIFEXITED(status) ) { /* child called exit() or returned from main() */ MESSAGE( "child exit status: %i", WEXITSTATUS(status) ); if ( WEXITSTATUS(status) == 0 ) { /* apparently normal termination */ MESSAGE( "child exited without error."); die_now = 1; continue; } else { MESSAGE("child exited abnormally."); nsm_proxy->handle_client_death(WEXITSTATUS(status)); } } } } int main ( int argc, char **argv ) { set_traps(); sigset_t mask; sigemptyset( &mask ); sigaddset( &mask, SIGCHLD ); sigprocmask(SIG_BLOCK, &mask, NULL ); signal_fd = signalfd( -1, &mask, SFD_NONBLOCK ); //Command line parameters static struct option long_options[] = { { "help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; int option_index = 0; int c = 0; while ( ( c = getopt_long_only( argc, argv, "", long_options, &option_index ) ) != -1 ) { switch ( c ) { case 'h': { const char *usage = "nsm-proxy - Wrapper for executables without direct NSM-Support.\n\n" "It is a module for the 'New Session Manager' and only communicates\n" "over OSC in an NSM-Session and has no standalone functionality.\n" "\n" "Usage:\n" " nsm-proxy --help\n" "\n" "Options:\n" " --help Show this screen\n" ""; puts ( usage ); exit(0); break; } } } nsm_proxy = new NSM_Proxy(); init_osc( NULL ); const char *nsm_url = getenv( "NSM_URL" ); if ( nsm_url ) { announce( nsm_url, APP_TITLE, argv[0] ); } else { fprintf( stderr, "Could not register as NSM client.\n" ); exit(1); } struct signalfd_siginfo fdsi; /* listen for sigchld signals and process OSC messages forever */ for ( ;; ) { ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo)); if (s == sizeof(struct signalfd_siginfo)) { if (fdsi.ssi_signo == SIGCHLD) handle_sigchld(); } lo_server_recv_noblock( losrv, 500 ); if ( die_now ) die(); } } 07070100000046000081A40000000000000000000000016258A65C00002546000000000000000000000000000000000000004100000000new-session-manager-1.6.0+git.20220415.0f6719c/src/nsm-proxy.svg<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="68.791656mm" height="68.791656mm" viewBox="0 0 68.791656 68.791656" version="1.1" id="svg8" sodipodi:docname="icons.svg" inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> <defs id="defs2" /> <sodipodi:namedview units="px" id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.7" inkscape:cx="-626.55231" inkscape:cy="350.73591" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" showgrid="false" inkscape:window-width="2558" inkscape:window-height="1398" inkscape:window-x="0" inkscape:window-y="20" inkscape:window-maximized="1" /> <metadata id="metadata5"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g transform="translate(-231.11632,-36.693907)" inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"> <g id="nsm" inkscape:label="nsm" transform="translate(-172.56864)"> <title id="title888">nsm</title> <rect y="114.63333" x="71.133331" height="67.73333" width="67.73333" id="rect10" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="105.32493" y="169.25755" id="text852"><tspan sodipodi:role="line" x="105.32493" y="169.25755" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan850">NSM</tspan></text> </g> <g transform="translate(-170.13191,80.919145)" inkscape:label="jackpatch" id="jackpatch"> <title id="title890">jackpatch</title> <rect style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect892" width="67.73333" height="67.73333" x="71.133331" y="114.63333" /> <text id="text896" y="165.8237" x="106.38719" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" xml:space="preserve"><tspan id="tspan894" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" y="165.8237" x="106.38719" sodipodi:role="line">JP</tspan></text> </g> <g id="proxy" inkscape:label="proxy"> <title id="title940">proxy</title> <rect y="37.223072" x="-99.065941" height="67.73333" width="67.73333" id="rect902" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" x="-65.639175" y="93.759422" id="text906"><tspan sodipodi:role="line" x="-65.639175" y="93.759422" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" id="tspan904">PRX</tspan></text> </g> <path d="m 231.64549,37.223072 h 67.73333 V 104.9564 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1679" /> <g style="font-style:normal;font-weight:normal;font-size:67.6707px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="text1683" aria-label="PRX"> <path id="path1690" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 238.98532,48.420053 0.0677,45.339369 h 4.87229 V 72.578493 l 0.27069,-0.06767 h 4.60161 c 1.42108,0 2.63915,-0.406024 3.65421,-1.150401 1.01506,-0.744378 1.48876,-1.962451 1.48876,-3.654218 V 53.427685 c 0,-1.96245 -0.4737,-3.315864 -1.42109,-3.992571 -0.94739,-0.676707 -2.16546,-1.015061 -3.72188,-1.015061 z m 4.93996,4.872291 h 3.31587 c 0.47369,0 0.81205,0.135341 1.1504,0.473695 0.33835,0.338353 0.4737,0.744377 0.4737,1.218072 v 10.962654 c 0,0.473694 -0.13535,0.812048 -0.4737,1.150401 -0.33835,0.338354 -0.67671,0.473695 -1.1504,0.473695 h -3.31587 z" /> <path id="path1692" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 262.66997,53.292344 h 3.31586 c 0.4737,0 0.81205,0.135341 1.15041,0.473695 0.33835,0.338353 0.47369,0.744377 0.47369,1.218072 v 10.962654 c 0,0.473694 -0.13534,0.812048 -0.47369,1.150401 -0.33836,0.338354 -0.67671,0.473695 -1.15041,0.473695 h -3.31586 z m -4.93996,-4.872291 0.0677,45.339369 H 262.67 v -21.2486 h 1.21807 l 4.19559,21.2486 h 4.87229 L 268.69266,72.24014 c 0.60904,-0.203012 1.42109,-0.609037 2.50382,-1.218073 0.94739,-0.609036 1.48875,-1.759438 1.48875,-3.451206 V 53.427685 c 0,-1.285743 -0.47369,-2.436145 -1.48875,-3.518876 -1.01506,-1.015061 -2.16547,-1.488756 -3.51888,-1.488756 z" /> <path id="path1694" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;text-anchor:middle;stroke-width:1.45009" d="m 275.6627,48.420053 5.0753,23.346392 -4.80462,21.789965 h 5.00763 l 2.90984,-13.53414 2.90984,13.53414 h 4.93996 l -4.73695,-21.789965 5.07531,-23.346392 h -5.14298 l -3.04518,14.752213 -3.04518,-14.752213 z" /> </g> <path d="m 230.75774,195.55247 h 67.73333 v 67.73333 h -67.73333 z" style="fill:#ffffff;stroke:#000000;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect1700" /> <path d="m 256.70029,252.35951 v -50.95604 h -5.0753 v 49.46729 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -1.75944 v 4.93996 h 3.04518 c 3.58655,0 5.41366,-1.69176 5.41366,-5.0753 z" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;line-height:1.25;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="path1712" /> <path d="m 266.05212,201.40347 0.0677,45.33937 h 4.87229 v -21.18093 l 0.27068,-0.0677 h 4.60161 c 1.42108,0 2.63916,-0.40602 3.65422,-1.1504 1.01506,-0.74438 1.48875,-1.96245 1.48875,-3.65422 v -14.27851 c 0,-1.96245 -0.47369,-3.31587 -1.42108,-3.99257 -0.94739,-0.67671 -2.16546,-1.01507 -3.72189,-1.01507 z m 4.93996,4.87229 h 3.31586 c 0.4737,0 0.81205,0.13535 1.1504,0.4737 0.33836,0.33835 0.4737,0.74438 0.4737,1.21807 v 10.96266 c 0,0.47369 -0.13534,0.81204 -0.4737,1.1504 -0.33835,0.33835 -0.6767,0.47369 -1.1504,0.47369 h -3.31586 z" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.6707px;line-height:1.25;font-family:'Sturkopf Grotesk';-inkscape-font-specification:'Sturkopf Grotesk';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.45009" id="path1714" /> </g> </svg> 07070100000047000081A40000000000000000000000016258A65C000149A7000000000000000000000000000000000000003C00000000new-session-manager-1.6.0+git.20220415.0f6719c/src/nsmd.cpp /*******************************************************************************/ /* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager") */ /* Copyright (C) 2020- Nils Hilbricht */ /* */ /* This file is part of New-Session-Manager */ /* */ /* New-Session-Manager is free software: you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation, either version 3 of the License, or */ /* (at your option) any later version. */ /* */ /* New-Session-Manager is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/ /*******************************************************************************/ #define __MODULE__ "nsmd" //debug.c has only one function that gets used multiple times by debug.h and for logging and printing #include "debug.h" #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <errno.h> #include <string.h> #include <list> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> #include <sys/signalfd.h> #include <sys/stat.h> #include <sys/wait.h> #include <time.h> #include <libgen.h> #include <list> #include <getopt.h> #include <sys/time.h> #include <fts.h> #include "Endpoint.hpp" /* for locking */ #include "file.h" #include <map> #include <string> #include <algorithm> #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wunused-result" static OSC::Endpoint *osc_server; static lo_address gui_addr; static bool gui_is_active = false; static int signal_fd; static char *session_root; static char *lockfile_directory; static char *daemon_file; #define NSM_API_VERSION_MAJOR 1 #define NSM_API_VERSION_MINOR 1 #define NSM_API_VERSION_PATCH 2 #define VERSION_STRING "1.6.0" #define ERR_OK 0 #define ERR_GENERAL_ERROR -1 #define ERR_INCOMPATIBLE_API -2 #define ERR_BLACKLISTED -3 #define ERR_LAUNCH_FAILED -4 #define ERR_NO_SUCH_FILE -5 #define ERR_NO_SESSION_OPEN -6 #define ERR_UNSAVED_CHANGES -7 #define ERR_NOT_NOW -8 #define ERR_BAD_PROJECT -9 #define ERR_CREATE_FAILED -10 #define ERR_SESSION_LOCKED -11 #define ERR_OPERATION_PENDING -12 #define APP_TITLE "New Session Manager" enum { COMMAND_NONE = 0, COMMAND_QUIT, COMMAND_KILL, COMMAND_SAVE, COMMAND_OPEN, COMMAND_START, COMMAND_CLOSE, COMMAND_DUPLICATE, COMMAND_NEW }; static int pending_operation = COMMAND_NONE; static void wait ( long ); void handle_signal_clean_exit ( int ); #define GUIMSG( fmt, args... ) \ { \ MESSAGE( fmt, ## args ); \ if ( gui_is_active ) \ { \ char *s;\ asprintf( &s, fmt, ## args );\ osc_server->send( gui_addr, "/nsm/gui/server/message", s);\ free(s);\ }\ } struct Client { private: int _reply_errcode; char *_reply_message; int _pending_command; struct timeval _command_sent_time; bool _gui_visible; char *_label; public: lo_address addr=0; /* */ char *name; /* First this is the basename of client executable, later it becomes the client-reported name which must be treated as if unrelated. */ char *executable_path; /* Contrary to the name this is basename(executable) */ int pid; /* PID of client process */ float progress; /* */ bool active; /* NSM capable: client has registered via announce */ //bool stopped; /* the client quit, but not because we told it to--user still has to decide to remove it from the session */ char *client_id; /* short part of client ID */ char *capabilities; /* client capabilities... will be null for dumb clients */ bool dirty; /* flag for client self-reported dirtiness */ bool pre_existing; const char *status; int launch_error; /* v1.4, leads to status for executable not found, permission denied etc. */ char *name_with_id; /* v1.4, client.nABC */ const char *label ( void ) const { return _label; } void label ( const char *l ) { if ( _label ) free( _label ); if ( l ) _label = strdup( l ); else _label = NULL; } bool gui_visible ( void ) const { return _gui_visible; } void gui_visible ( bool b ) { _gui_visible = b; } bool has_error ( void ) const { return _reply_errcode != 0; } int error_code ( void ) const { return _reply_errcode; } const char * message ( void ) { return _reply_message; } void set_reply ( int errcode, const char *message ) { if ( _reply_message ) free( _reply_message ); _reply_message = strdup( message ); _reply_errcode = errcode; } bool reply_pending ( void ) { return _pending_command != COMMAND_NONE; } bool is_dumb_client ( void ) { return capabilities == NULL; } void pending_command ( int command ) { gettimeofday( &_command_sent_time, NULL ); _pending_command = command; } double milliseconds_since_last_command ( void ) const { struct timeval now; gettimeofday( &now, NULL ); double elapsedms = ( now.tv_sec - _command_sent_time.tv_sec ) * 1000.0; elapsedms += ( now.tv_usec - _command_sent_time.tv_usec ) / 1000.0; return elapsedms; } int pending_command ( void ) { return _pending_command; } // capability should be enclosed in colons. I.e. ":switch:" bool is_capable_of ( const char *capability ) const { return capabilities && strstr( capabilities, capability ); } Client ( ) { _label = 0; _gui_visible = true; _reply_errcode = 0; _reply_message = 0; pid = 0; progress = -0; _pending_command = 0; active = false; client_id = 0; capabilities = 0; name = 0; executable_path = 0; pre_existing = false; launch_error = 0; dirty = 0; status = 0; name_with_id = 0; } ~Client ( ) { if ( name ) free(name); if (executable_path) free(executable_path); if (client_id) free(client_id); if (capabilities) free(capabilities); if (name_with_id) free(name_with_id); name = executable_path = client_id = capabilities = name_with_id = NULL; } }; static std::list< Client* > client; /* helper macros for defining OSC handlers */ #define OSC_NAME( name ) osc_ ## name #define OSC_HANDLER( name ) static int OSC_NAME( name ) ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) static char *session_path = NULL; static char *session_name = NULL; bool clients_have_errors ( ) { for ( std::list<Client*>::const_iterator i = client.begin(); i != client.end(); ++i ) if ( (*i)->active && (*i)->has_error() ) return true; return false; } Client * get_client_by_pid ( int pid ) { std::list<Client*> *cl = &client; for ( std::list<Client*>::const_iterator i = cl->begin(); i != cl->end(); ++i ) if ( (*i)->pid == pid ) return *i; return NULL; } void clear_clients ( void ) { std::list<Client*> *cl = &client; for ( std::list<Client*>::iterator i = cl->begin(); i != cl->end(); ++i ) { delete *i; i = cl->erase( i ); } } void handle_client_process_death ( int pid ) { Client *c = get_client_by_pid( (int)pid ); if ( c ) { //There is a difference if a client quit on its own, e.g. via a menu or window manager, //or if the server send SIGTERM as quit signal. Both cases are equally valid. //We only check the case to print a different log message bool dead_because_we_said = ( c->pending_command() == COMMAND_KILL || c->pending_command() == COMMAND_QUIT ); if ( dead_because_we_said ) { GUIMSG( "Client %s terminated by server instruction.", c->name_with_id ); } else { GUIMSG( "Client %s terminated itself.", c->name_with_id ); } //Decide if the client terminated or if removed from the session if ( c->pending_command() == COMMAND_QUIT ) { c->status = "removed"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); client.remove(c); //This will not remove the clients save data delete c; } else { if ( c->launch_error ) /* NSM API treats the stopped status as switch. You can only remove stopped. * Furthermore the GUI will change its client-buttons. * In consequence we cannot add an arbitrary "launch-error" status. * Compatible compromise is to use the label field to relay info the user, * which was the goal. There is nothing we can do about a failed launch anyway. */ c->label( "launch error!" ); else c->label( "" ); c->status = "stopped"; if ( gui_is_active ) { osc_server->send( gui_addr, "/nsm/gui/client/label", c->client_id, c->label() ); osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); } } c->pending_command( COMMAND_NONE ); c->active = false; c->pid = 0; } } void handle_sigchld ( ) { // compare waitpid(2) for ( ;; ) { int status = 1; // make it not NULL to enable information storage in status pid_t pid = waitpid(-1, &status, WNOHANG); //-1 meaning wait for any child process. pid_t is signed integer if (pid <= 0) { break; // no child process has ended this loop. Check again. } else { //One child process has stopped. Find which and figure out the stop-conditions Client *c; c = get_client_by_pid( pid ); if ( c ) { //The following will not trigger with normal crashes, e.g. segfaults or python tracebacks if ( WIFEXITED( status ) ) // returns true if the child terminated normally if ( WEXITSTATUS( status ) == 255 ) // as given by exit(-1) in launch() c->launch_error = true; } // Call even if Client was already null. This will check itself again and was expected // to be called for the majority of nsmds development handle_client_process_death( pid ); } } } int path_is_valid ( const char *path ) { char *s; asprintf( &s, "/%s/", path ); int r = strstr( s, "/../" ) == NULL; free( s ); return r; } int session_already_exists ( const char * relative_session_path) { //A session is defined as a path with the file session.nsm //We receive the relative path with sub-directories like album/song as relative_session_path, without leading and trailing / struct stat st_session_exists_check; char * path; asprintf( &path, "%s/%s/session.nsm", session_root, relative_session_path ); if ( stat (path, &st_session_exists_check) == 0) { free( path ); return 0; // Already exists } else { free( path ); return -1; // All good } } int mkpath ( const char *path, bool create_final_directory ) { char *p = strdup( path ); char *i = p + 1; while ( ( i = index( i, '/' ) ) ) { *i = 0; struct stat st; if ( stat( p, &st ) ) { if ( mkdir( p, 0711 ) ) { free( p ); return -1; } } *i = '/'; i++; } if ( create_final_directory ) { if ( mkdir( p, 0711 ) ) { free( p ); return -1; } } free( p ); return 0; } void set_name ( const char *name ) { if ( session_name ) free( session_name ); char *s = strdup( name ); session_name = strdup( basename( s ) ); free( s ); } bool address_matches ( lo_address addr1, lo_address addr2 ) { /* char *url1 = lo_address_get_url( addr1 ); */ /* char *url2 = lo_address_get_url( addr2 ); */ char *url1 = strdup( lo_address_get_port( addr1 ) ); char *url2 = strdup(lo_address_get_port( addr2 ) ); bool r = !strcmp( url1, url2 ); free( url1 ); free( url2 ); return r; } Client * get_client_by_id ( std::list<Client*> *cl, const char *id ) { for ( std::list<Client*>::const_iterator i = cl->begin(); i != cl->end(); ++i ) if ( !strcmp( (*i)->client_id, id ) ) return *i; return NULL; } Client * get_client_by_name_and_id ( std::list<Client*> *cl, const char *name, const char *id ) { for ( std::list<Client*>::const_iterator i = cl->begin(); i != cl->end(); ++i ) if ( !strcmp( (*i)->client_id, id ) && ! strcmp( (*i)->name, name ) ) return *i; return NULL; } Client * get_client_by_address ( lo_address addr ) { for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) if ( (*i)->addr && address_matches( (*i)->addr, addr ) ) return *i; return NULL; } char * generate_client_id ( void ) { /* Before v1.4 this returned "n" + 4 random upper-case letters, which could lead to collisions. We changed behaviour to still generate 4 letters, but check for collision with existing IDs. Loaded client IDs are not checked, just copied from session.nsm because loading happens before any generation of new clients. Loaded clients are part of further checks of course. There is a theoretical limit when all 26^4 IDs are in use which will lead to an infinite loop of generation. We risk to leave this unhandled. */ char id_str[6]; id_str[0] = 'n'; id_str[5] = 0; while ( true ) { for ( int i = 1; i < 5; i++ ) id_str[i] = 'A' + (rand() % 25); if ( get_client_by_id(&client, id_str )==NULL ) // found a free id break; } return strdup(id_str); } bool replies_still_pending ( void ) { for ( std::list<Client*>::const_iterator i = client.begin(); i != client.end(); ++i ) if ( (*i)->active && (*i)->reply_pending() ) return true; return false; } int number_of_reponsive_clients ( void ) { /* This was renamed from number_of_active_clients in version 1.4 to reflect * that not only active==true clients are in a state where waiting has ended, but also clients * that never started. It is used in wait_for_announce only, which added a 5000ms delay to startup * * We are sadly unable to distinguish between a client that has a slow announce and a client * without NSM-support. However, this is mitigated by nsm-proxy which is a reliable indicator * that this program will never announce (or rather nsm-proxy announces normally). */ int responsive = 0; for ( std::list<Client*>::const_iterator i = client.begin(); i != client.end(); ++i ) { //Optimisation: Clients that never launched (e.g. file not found) will be checked many times/seconds here. We skip them by counting them if ( (*i)->active || (*i)->launch_error ) responsive++; } return responsive; } void wait_for_announce ( void ) { GUIMSG( "Waiting for announce messages from clients" ); int n = 5 * 1000; long unsigned int active; while ( n > 0 ) { n -= 100; wait(100); active = number_of_reponsive_clients(); if ( client.size() == active ) break; } GUIMSG( "Done. %lu out of %lu clients announced (or failed to launch) within the initialization grace period", active, (long unsigned)client.size() ); } void wait_for_replies ( void ) { GUIMSG( "Waiting for clients to reply to commands" ); int n = 60 * 1000; /* 60 seconds */ while ( n ) { n -= 100; wait(100); if ( ! replies_still_pending() ) break; } GUIMSG( "Done waiting" ); /* FIXME: do something about unresponsive clients */ } char * get_client_project_path ( const char *session_path, Client *c ) { char *client_project_path; asprintf( &client_project_path, "%s/%s.%s", session_path, c->name, c->client_id ); return client_project_path; } bool launch ( const char *executable, const char *client_id ) { Client *c; if ( !client_id || !( c = get_client_by_id( &client, client_id ) ) ) { c = new Client(); c->executable_path = strdup( executable ); { char *s = strdup( c->executable_path ); c->name = strdup( basename( s ) ); free( s ); } if ( client_id ) c->client_id = strdup( client_id ); else c->client_id = generate_client_id(); asprintf( &c->name_with_id, "%s.%s", c->name, c->client_id ); client.push_back( c ); } char * url = osc_server->url(); int pid; if ( ! (pid = fork()) ) { //This is code of the child process. It will be executed after launch() has finished GUIMSG( "Launching %s", executable ); char *args[] = { strdup( executable ), NULL }; setenv( "NSM_URL", url, 1 ); /* Ensure the launched process can receive SIGCHLD */ /* Unblocking SIGCHLD here does NOT unblock it for nsmd itself */ sigset_t mask; sigemptyset( &mask ); sigaddset( &mask, SIGCHLD ); sigprocmask(SIG_UNBLOCK, &mask, NULL ); if ( -1 == execvp( executable, args ) ) { /* The program was not started. Causes: not installed on the current system, and the * session was transferred from another system, or permission denied (no executable flag) * Since we are running in a forked child process Client c does exist, but points to * a memory copy, not the real client. So we can't set any error code or status in the * client object. Instead we check the exit return code in handle_sigchld() and set the * bool client->launch_error to true. */ WARNING( "Error starting process %s: %s", executable, strerror( errno ) ); exit(-1); //-1 later parsed as 255 } } //This is code of the parent process. It is executed right at this point, before the child. c->pending_command( COMMAND_START ); c->pid = pid; MESSAGE( "Process %s has pid: %i", executable, pid ); //We do not have a name yet, use executable //Normal launch. Setting launch_error to false is not redundant: //A previous launch-error fixed by the user, and then resume, needs this reset. c->launch_error = false; c->status = "launch"; if ( gui_is_active ) { //At this point we do not know if launched program will start or fail //And we do not know if it has nsm-support or not. This will be decided if it announces. osc_server->send( gui_addr, "/nsm/gui/client/new", c->client_id, c->executable_path ); // a second message may get send with c->name, if the client sends announce() osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); osc_server->send( gui_addr, "/nsm/gui/client/label", c->client_id, "" ); } return true; } void command_client_to_save ( Client *c ) { if ( c->active ) { MESSAGE( "Telling %s to save", c->name_with_id ); osc_server->send( c->addr, "/nsm/client/save" ); c->pending_command( COMMAND_SAVE ); c->status = "save"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); } else if ( c->is_dumb_client() && c->pid ) { // this is a dumb client... c->status = "noop"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); } } void command_client_to_switch ( Client *c, const char *new_client_id ) { char *old_client_id = c->client_id; c->client_id = strdup( new_client_id ); char *client_project_path = get_client_project_path( session_path, c ); MESSAGE( "Commanding %s to switch \"%s\"", c->name_with_id, client_project_path ); char *full_client_id; asprintf( &full_client_id, "%s.%s", c->name, c->client_id ); osc_server->send( c->addr, "/nsm/client/open", client_project_path, session_name, full_client_id ); free( full_client_id ); free( client_project_path ); c->pending_command( COMMAND_OPEN ); c->status = "switch"; if ( gui_is_active ) { osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); osc_server->send( gui_addr, "/nsm/gui/client/switch", old_client_id, c->client_id ); } free( old_client_id ); } void purge_inactive_clients ( ) { for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { if ( ! (*i)->active ) { (*i)->status = "removed"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", (*i)->client_id, (*i)->status ); delete *i; i = client.erase( i ); } } } bool process_is_running ( int pid ) { if ( 0 == kill( pid, 0 ) ) { return true; } else if ( ESRCH == errno ) { return false; } return false; } void purge_dead_clients ( ) { std::list<Client*> tmp( client ); for ( std::list<Client*>::const_iterator i = tmp.begin(); i != tmp.end(); ++i ) { const Client *c = *i; if ( c->pid ) { if ( ! process_is_running( c->pid ) ) handle_client_process_death( c->pid ); } } } /************************/ /* OSC Message Handlers */ /************************/ OSC_HANDLER( add ) { if ( ! session_path ) { osc_server->send( lo_message_get_source( msg ), "/error", path, ERR_NO_SESSION_OPEN, "Cannot add to session because no session is loaded." ); return 0; } if ( strchr( &argv[0]->s, '/' ) ) { osc_server->send( lo_message_get_source( msg ), "/error", path, ERR_LAUNCH_FAILED, "Absolute paths are not permitted. Clients must be in $PATH" ); return 0; } if ( ! launch( &argv[0]->s, NULL ) ) { osc_server->send( lo_message_get_source( msg ), "/error", path, ERR_LAUNCH_FAILED, "Failed to launch process!" ); } else { osc_server->send( lo_message_get_source( msg ), "/reply", path, "Launched." ); } return 0; } OSC_HANDLER( announce ) { /* A client announces itself which identifies it as real nsm-capable client, internally represented by the c->active bool. If nsmd started the client itself (e.g. through a GUI) at this point the program is already part of the session and registered with c->name=basename(executable). For these clients a second client/new message is sent, indicating an upgrade of the formerly dumb client. Through this c->name changes from executable to the self-reported client name from this announce message. Before v1.4 clients that announce themselves (started with NSM URL ENV present) never triggered the first client/new which sends an executable. This created a problem with attaching GUIs to a running nsmd never were able to infer any data from executables, like icons. Changed so that every new client scenario sends basename(executable) first. */ const char *client_name = &argv[0]->s; const char *capabilities = &argv[1]->s; const char *executable_path = &argv[2]->s; int major = argv[3]->i; int minor = argv[4]->i; int pid = argv[5]->i; GUIMSG( "Got announce from %s", client_name ); if ( ! session_path ) { osc_server->send( lo_message_get_source( msg ), "/error", path, ERR_NO_SESSION_OPEN, "Sorry, but there's no session open for this application to join." ); return 0; } bool expected_client = false; Client *c = NULL; for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { if ( ! strcmp( (*i)->executable_path, executable_path ) && ! (*i)->active && (*i)->pending_command() == COMMAND_START ) { // I think we've found the slot we were looking for. MESSAGE( "Client %s was expected.", (*i)->name ); c = *i; break; } } if ( ! c ) { c = new Client(); c->executable_path = strdup( executable_path ); c->client_id = generate_client_id(); } else expected_client = true; if ( major > NSM_API_VERSION_MAJOR ) { MESSAGE( "Client %s is using incompatible and more recent API version %i.%i", c->name_with_id, major, minor ); osc_server->send( lo_message_get_source( msg ), "/error", path, ERR_INCOMPATIBLE_API, "Server is using an incompatible API version." ); return 0; } c->pid = pid; c->capabilities = strdup( capabilities ); c->addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); c->name = strdup( client_name ); //replace executable with clients self-reported pretty name c->active = true; asprintf( &c->name_with_id, "%s.%s", c->name, c->client_id ); MESSAGE( "Process %s has pid: %i", c->name_with_id, pid ); if ( ! expected_client ) client.push_back( c ); MESSAGE( "The client \"%s\" at \"%s\" informs us it's ready to receive commands.", &argv[0]->s, lo_address_get_url( c->addr ) ); osc_server->send( lo_message_get_source( msg ), "/reply", path, expected_client ? "Acknowledged as full NSM client (started ourselves)." : "Acknowledged as full NSM client (registered itself from the outside).", APP_TITLE, ":server-control:broadcast:optional-gui:" ); c->status = "open"; if ( gui_is_active ) { osc_server->send( gui_addr, "/nsm/gui/client/new", c->client_id, c->name ); //pretty-name. not exectuable osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); if ( c->is_capable_of( ":optional-gui:" ) ) osc_server->send( gui_addr, "/nsm/gui/client/has_optional_gui", c->client_id ); } { char *full_client_id; asprintf( &full_client_id, "%s.%s", c->name, c->client_id ); char *client_project_path = get_client_project_path( session_path, c ); osc_server->send( lo_message_get_source( msg ), "/nsm/client/open", client_project_path, session_name, full_client_id ); c->pending_command( COMMAND_OPEN ); free( full_client_id ); free( client_project_path ); } return 0; } int save_session_file ( ) { char *session_file = NULL; asprintf( &session_file, "%s/session.nsm", session_path ); FILE *fp = fopen( session_file, "w" ); if ( fp == NULL ) { WARNING( "No write access to %s with error: %s.", session_file, strerror( errno ) ); free( session_file ); //No need to fclose because fp is null and was never opened. return 1; //error } free( session_file ); for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { fprintf( fp, "%s:%s:%s\n", (*i)->name, (*i)->executable_path, (*i)->client_id ); } fclose( fp ); return 0; //all ok } Client * client_by_name ( const char *name, std::list<Client*> *cl ) { for ( std::list<Client*>::iterator i = cl->begin(); i != cl->end(); ++i ) { if ( !strcmp( name, (*i)->name ) ) return *i; } return NULL; } bool dumb_clients_are_alive ( ) { std::list<Client*> *cl = &client; for ( std::list<Client*>::iterator i = cl->begin(); i != cl->end(); ++i ) { if ( (*i)->is_dumb_client() && (*i)->pid > 0 ) { MESSAGE( "Waiting for %s", (*i)->name_with_id ); //This replaced the Loop 1, Loop 2 ... 60 message from wait_for_dumb_clients_to_die where you couldn't see which client actually was hanging return true; } } return false; } void wait_for_dumb_clients_to_die ( ) { struct signalfd_siginfo fdsi; GUIMSG( "Waiting for any dumb clients to die." ); for ( int i = 0; i < 6; i++ ) { if ( ! dumb_clients_are_alive() ) break; ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo)); if (s == sizeof(struct signalfd_siginfo)) { if (fdsi.ssi_signo == SIGCHLD) handle_sigchld(); } usleep( 50000 ); } GUIMSG( "Done waiting" ); /* FIXME: give up on remaining clients and purge them */ } bool killed_clients_are_alive ( ) { std::list<Client*> *cl = &client; for ( std::list<Client*>::iterator i = cl->begin(); i != cl->end(); ++i ) { if ( ( (*i)->pending_command() == COMMAND_QUIT || (*i)->pending_command() == COMMAND_KILL ) && (*i)->pid > 0 ) { MESSAGE( "Waiting for %s", (*i)->name_with_id ); //This replaced the Loop 1, Loop 2 ... 60 message from wait_for_killed_clients_to_die where you couldn't see which client actually was hanging return true; } } return false; } void wait_for_killed_clients_to_die ( ) { struct signalfd_siginfo fdsi; MESSAGE( "Waiting for killed clients to die." ); for ( int i = 0; i < 60; i++ ) { if ( ! killed_clients_are_alive() ) goto done; ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo)); if (s == sizeof(struct signalfd_siginfo)) { if (fdsi.ssi_signo == SIGCHLD) handle_sigchld(); } purge_dead_clients(); /* check OSC so we can get /progress messages. */ osc_server->check(); sleep(1); } WARNING( "Killed clients are still alive" ); return; done: MESSAGE( "All clients have died." ); } void command_all_clients_to_save ( ) { if ( session_path ) { GUIMSG( "Commanding attached clients to save." ); int save_error = save_session_file(); if ( save_error == 1 ) { GUIMSG( "...but the session file is write protected. Will not forward save command to clients." ); WARNING( "Aborting client save commands because the session file is write protected" ); return; } for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { command_client_to_save( *i ); } wait_for_replies(); } } void command_client_to_stop ( Client *c ) { GUIMSG( "Stopping client %s", c->name_with_id ); if ( c->pid > 0 ) { c->pending_command( COMMAND_KILL ); kill( c->pid, SIGTERM ); c->status = "stopped"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); } } void command_client_to_quit ( Client *c ) { MESSAGE( "Commanding %s to quit", c->name_with_id ); if ( c->active ) { c->pending_command( COMMAND_QUIT ); kill( c->pid, SIGTERM ); c->status = "quit"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); } else if ( c->is_dumb_client() ) { if ( c->pid > 0 ) { c->status = "quit"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); /* should be kill? */ c->pending_command( COMMAND_QUIT ); // this is a dumb client... try and kill it kill( c->pid, SIGTERM ); } else { c->status = "removed"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); } } } char * get_lock_file_name( const char * session_name, const char * full_absolute_session_path ) { // To avoid collisions of two simple session names under either different subdirs o even different session roots. char *session_hash = simple_hash( full_absolute_session_path ); char *session_lock; asprintf( &session_lock, "%s/%s%s", lockfile_directory, session_name, session_hash ); //lockfile_directory and session_name are variables in the current context. free(session_hash); return session_lock; } void write_lock_file( const char *filename, const char * session_path ) { //Not a GNU lockfile, which features were never used by nsmd anyway, //but simply a file with information about the NSM Server and the loaded session FILE *fp = fopen( filename, "w" ); if ( !fp ) { FATAL( "Failed to write lock file to %s with error: %s", filename, strerror( errno ) ); } fprintf( fp, "%s\n%s\n%d\n", session_path, osc_server->url(), getpid()); MESSAGE( "Created lock file %s", filename ); fclose( fp ); } void delete_lock_file( const char *filename ) { unlink( filename ); MESSAGE( "Deleted lock file %s", filename ); } void close_session ( ) { if ( ! session_path ) return; for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { command_client_to_quit( *i ); } wait_for_killed_clients_to_die(); purge_inactive_clients(); clear_clients(); if ( session_path ) { char * session_lock = get_lock_file_name( session_name, session_path); delete_lock_file( session_lock ); MESSAGE( "Session %s was closed.", session_path ); free(session_lock); free(session_path); session_path = NULL; free(session_name); session_name = NULL; } if ( gui_is_active ) { osc_server->send( gui_addr, "/nsm/gui/session/name", "", "" ); //Empty string = no current session } } void tell_client_session_is_loaded( Client *c ) { if ( c->active ) //!c->is_dumb_client() ) { MESSAGE( "Telling client %s that session is loaded.", c->name_with_id ); osc_server->send( c->addr, "/nsm/client/session_is_loaded" ); } } void tell_all_clients_session_is_loaded ( void ) { MESSAGE( "Telling all clients that session is loaded..." ); for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { tell_client_session_is_loaded( *i ); } } int load_session_file ( const char * path ) { //parameter "path" is the absolute path to the session including session root, without session.nsm //First check if the session file actually exists, before closing the current one const char * relative_session_path = strdup( path ) + strlen( session_root ) + 1; //+1 for trailing / if ( session_already_exists( relative_session_path ) != 0) { WARNING ( "Instructed to load %s which does not exist. Doing nothing.", path ); return ERR_NO_SUCH_FILE; } if ( session_path && session_name ) { //We are already in a session. This is switch, or load during duplicate etc. MESSAGE ( "Instructed to load %s while %s is still open. This is a normal operation. Attempting to switch clients intelligently, if they support it. Otherwise closing and re-opening.", path, session_path ); char * session_lock = get_lock_file_name( session_name, session_path); delete_lock_file( session_lock ); } set_name( path ); //Do this first so we have the simple name name for lockfiles and log messages char *session_file = NULL; asprintf( &session_file, "%s/session.nsm", path ); //Check if the lockfile already exists, which means another nsmd currently has loaded the session we want to load. char * session_lock = get_lock_file_name( session_name, path ); struct stat st_lockfile_exists_check; if ( stat (session_lock, &st_lockfile_exists_check) == 0) { WARNING( "Session %s is already loaded from another nsmd and locked by file %s", session_name, session_lock ); free( session_file ); free( session_lock ); return ERR_SESSION_LOCKED; } FILE *fp; if ( ! ( fp = fopen( session_file, "r" ) ) ) { free( session_file ); return ERR_CREATE_FAILED; } free( session_file ); session_path = strdup( path ); std::list<Client*> new_clients; { char * client_name = NULL; char * client_executable = NULL; char * client_id = NULL; // load the client list while ( fscanf( fp, "%m[^:]:%m[^:]:%m[^:\n]\n", &client_name, &client_executable, &client_id ) > 0 ) { Client *c = new Client(); c->name = client_name; c->executable_path = client_executable; c->client_id = client_id; asprintf( &c->name_with_id, "%s.%s", c->name, c->client_id ); new_clients.push_back( c ); } } fclose(fp); MESSAGE( "Commanding unneeded and dumb clients to quit" ); std::map<std::string,int> client_map; /* count how many instances of each client are needed in the new session */ for ( std::list<Client*>::iterator i = new_clients.begin(); i != new_clients.end(); ++i ) { if ( client_map.find( (*i)->name) != client_map.end() ) client_map[(*i)->name]++; else client_map[(*i)->name] = 1; } for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { if ( ! (*i)->is_capable_of( ":switch:" ) || client_map.find((*i)->name ) == client_map.end() ) { /* client is not capable of switch, or is not wanted in the new session */ command_client_to_quit( *i ); } else { /* client is switch capable and may be wanted in the new session */ if ( client_map[ (*i)->name ]-- <= 0 ) /* nope,, we already have as many as we need, stop this one */ command_client_to_quit( *i ); } } // wait_for_replies(); wait_for_killed_clients_to_die(); // wait_for_dumb_clients_to_die(); purge_inactive_clients(); for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { (*i)->pre_existing = true; } MESSAGE( "Commanding smart clients to switch" ); for ( std::list<Client*>::iterator i = new_clients.begin(); i != new_clients.end(); ++i ) { Client *c = NULL; /* in a duplicated session, clients will have the same * IDs, so be sure to pick the right one to avoid race * conditions in JACK name registration. */ c = get_client_by_name_and_id( &client, (*i)->name, (*i)->client_id ); if ( ! c ) c = client_by_name( (*i)->name, &client ); if ( c && c->pre_existing && !c->reply_pending() ) { // since we already shutdown clients not capable of 'switch', we can assume that these are. command_client_to_switch( c, (*i)->client_id ); } else { /* sleep a little bit because liblo derives its sequence * of port numbers from the system time (second * resolution) and if too many clients start at once they * won't be able to find a free port. */ usleep( 100 * 1000 ); launch( (*i)->executable_path, (*i)->client_id ); } } /* this part is a little tricky... the clients need some time to * send their 'announce' messages before we can send them 'open' * and know that a reply is pending and we should continue waiting * until they finish. wait_for_replies() must check for OSC * messages immediately, even if no replies seem to be pending * yet. */ /* dumb clients will never send an 'announce message', so we need * to give up waiting on them fairly soon. */ wait_for_announce(); wait_for_replies(); tell_all_clients_session_is_loaded(); //We already checked if the logfile exists above, and it didn't. //We also tested for write permissions to our XDG run-dir, which we confirmed to have. //We can create the lockfile now. write_lock_file( session_lock, session_path ); MESSAGE( "Session %s was loaded.", session_path); new_clients.clear(); if ( gui_is_active ) { //This is not the case when --load-session was used. GUI announce will come later. //Send two parameters to signal that the session was loaded: simple session-name, relative session path below session root MESSAGE( "Informing GUI about running session name: %s with relative path %s", session_name, session_path + strlen( session_root ) ); osc_server->send( gui_addr, "/nsm/gui/session/name", session_name, session_path + strlen( session_root )); } return ERR_OK; } OSC_HANDLER( save ) { lo_address sender_addr; sender_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); if ( pending_operation != COMMAND_NONE ) { osc_server->send( sender_addr, "/error", path, ERR_OPERATION_PENDING, "An operation pending." ); return 0; } if ( ! session_path ) { osc_server->send( sender_addr, "/error", path, ERR_NO_SESSION_OPEN, "No session to save."); goto done; } command_all_clients_to_save(); MESSAGE( "Done." ); osc_server->send( sender_addr, "/reply", path, "Saved." ); done: pending_operation = COMMAND_NONE; return 0; } OSC_HANDLER( duplicate ) { lo_address sender_addr; sender_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); if ( pending_operation != COMMAND_NONE ) { osc_server->send( sender_addr, "/error", path, ERR_OPERATION_PENDING, "An operation pending." ); return 0; } pending_operation = COMMAND_DUPLICATE; if ( ! session_path ) { osc_server->send( sender_addr, "/error", path, ERR_NO_SESSION_OPEN, "No session to duplicate."); goto done; } if ( ! path_is_valid( &argv[0]->s ) ) { osc_server->send( sender_addr, "/error", path, ERR_CREATE_FAILED, "Invalid session name." ); goto done; } if ( session_already_exists(&argv[0]->s) == 0) { osc_server->send( sender_addr, "/error", path, ERR_CREATE_FAILED, "Session name already exists." ); pending_operation = COMMAND_NONE; return 0; } command_all_clients_to_save(); if ( clients_have_errors() ) { osc_server->send( sender_addr, "/error", path, ERR_GENERAL_ERROR, "Some clients could not save" ); goto done; } // save_session_file(); char *spath; asprintf( &spath, "%s/%s", session_root, &argv[0]->s ); mkpath( spath, false ); /* FIXME: code a recursive copy instead of calling the shell */ char *cmd; asprintf( &cmd, "cp -R \"%s\" \"%s\"", session_path, spath); system( cmd ); free( cmd ); osc_server->send( gui_addr, "/nsm/gui/session/session", &argv[0]->s ); MESSAGE( "Attempting to open during DUPLICATE: %s", spath ); //The original session is still open. load_session_file will close it, and possibly ::switch:: if ( !load_session_file( spath ) ) { MESSAGE( "Loaded" ); osc_server->send( sender_addr, "/reply", path, "Loaded." ); } else { MESSAGE( "Failed" ); osc_server->send( sender_addr, "/error", path, ERR_NO_SUCH_FILE, "No such file." ); free(spath); pending_operation = COMMAND_NONE; return -1; } free( spath ); MESSAGE( "Done" ); osc_server->send( sender_addr, "/reply", path, "Duplicated." ); done: pending_operation = COMMAND_NONE; return 0; } OSC_HANDLER( new ) { lo_address sender_addr; sender_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); if ( pending_operation != COMMAND_NONE ) { osc_server->send( sender_addr, "/error", path, ERR_OPERATION_PENDING, "An operation pending." ); return 0; } pending_operation = COMMAND_NEW; if ( ! path_is_valid( &argv[0]->s ) ) { osc_server->send( sender_addr, "/error", path, ERR_CREATE_FAILED, "Invalid session name." ); pending_operation = COMMAND_NONE; return 0; } if ( session_already_exists(&argv[0]->s) == 0) { osc_server->send( sender_addr, "/error", path, ERR_CREATE_FAILED, "Session name already exists." ); pending_operation = COMMAND_NONE; return 0; } if ( session_path ) //Already a session running? { command_all_clients_to_save(); close_session(); } GUIMSG( "Creating new session \"%s\"", &argv[0]->s ); char *spath; asprintf( &spath, "%s/%s", session_root, &argv[0]->s ); if ( mkpath( spath, true ) ) { osc_server->send( sender_addr, "/error", path, ERR_CREATE_FAILED, "Could not create the session directory" ); free(spath); pending_operation = COMMAND_NONE; return 0; } session_path = strdup( spath ); set_name( session_path ); char * session_lock = get_lock_file_name( session_name, session_path); write_lock_file( session_lock, session_path ); free ( session_lock ); osc_server->send( sender_addr, "/reply", path, "Created." ); if ( gui_is_active ) { osc_server->send( gui_addr, "/nsm/gui/session/session", &argv[0]->s ); //Send two parameters to signal that the session was loaded: simple session-name, relative session path below session root MESSAGE( "Informing GUI about running session name: %s with relative path %s", session_name, session_path + strlen( session_root ) ); osc_server->send( gui_addr, "/nsm/gui/session/name", session_name, session_path + strlen( session_root )); } save_session_file(); free( spath ); osc_server->send( sender_addr, "/reply", path, "Session created" ); pending_operation = COMMAND_NONE; return 0; } int fts_comparer_to_process_files_before_dirs( const FTSENT ** first, const FTSENT ** second ) { /* The argument compar() specifies a user-defined function which may be used to order the traversal of the hierarchy. It takes two pointers to pointers to FTSENT structures as arguments and should return a negative value, zero, or a positive value to indicate if the file referenced by its first argument comes before, in any order with respect to, or after, the file referenced by its second argument. The fts_accpath, fts_path, and fts_pathlen fields of the FTSENT structures may never be used in this comparison. If the fts_info field is set to FTS_NS or FTS_NSOK, the fts_statp field may not either. If the compar() argument is NULL, the directory traversal order is in the order listed in path_argv for the root paths, and in the order listed in the directory for everything else. */ if ( (*first)->fts_info & FTS_F ) return (-1); //first else if ( (*second)->fts_info & FTS_F ) return (1); //last else return strcmp((*first)->fts_name, (*second)->fts_name); //return (0); //doesn't matter } static lo_address list_response_address; OSC_HANDLER( list ) { //Parse the session_root recursively for session.nsm files and send names with /nsm/server/list //Sessions can be structured with sub-directories. //The file session.nsm marks a real session and is a 'leaf' of the session tree. //No other sessions are allowed below a dir containing session.nsm . GUIMSG( "Listing sessions" ); list_response_address = lo_message_get_source( msg ); //Use fts to walk the session_root /* An array of paths to traverse. Each path must be null * terminated and the list must end with a NULL pointer. */ char *paths[] = { session_root, NULL }; /* 2nd parameter: An options parameter. Must include either FTS_PHYSICAL or FTS_LOGICAL---they change how symbolic links are handled. Last parameter is a comparator which you can optionally provide to change the traversal of the filesystem hierarchy. Our comparator processes files before directories, so we can depend on that to remember if we are already in a session-dir. */ FTS *ftsp = fts_open(paths, FTS_LOGICAL, fts_comparer_to_process_files_before_dirs); if(ftsp == NULL) { FATAL( "fts_open" ); exit(EXIT_FAILURE); } FTSENT * currentSession = NULL; while( 1 ) // call fts_read() enough times to get each file { FTSENT *ent = fts_read(ftsp); // get next entry (could be file or directory). if( ent == NULL ) { if( errno == 0 ) break; // No more items, bail out of while loop else { // fts_read() had an error. FATAL( "fts_read" ); exit(EXIT_FAILURE); } } // Handle Types of Files // Given a "entry", determine if it is a file or directory if( ent->fts_info & FTS_D ) // We are entering into a directory { //printf( "Enter dir: %s\n", ent->fts_path ); if ( currentSession != NULL ) { //printf( "already found current session.nsm: %s . Waiting to leave dir. Ignoring %s\n", currentSession->fts_name, ent->fts_path ); // Setup that no descendants of this file are visited. int err = fts_set( ftsp, ent, FTS_SKIP ); if ( err != 0 ) { FATAL( "fts_set" ); exit(EXIT_FAILURE); } } } else if( ent->fts_info & FTS_DP ) // We are exiting a directory { //printf( "Exit dir: %s\n", ent->fts_path ); if ( ent == currentSession ) { //printf( "Exit current session dir: %s\n", ent->fts_path ); currentSession = NULL; } } else if( ent->fts_info & FTS_F ) // The entry is a file. { //printf( "File: %s\n", ent->fts_path ); if ( ! strcmp( "session.nsm", basename( ent->fts_path ) ) ) { //Convert path to session name: char *s; s = strdup( ent->fts_path ); s = dirname( s ); memmove( s, s + strlen( session_root ) + 1, (strlen( s ) - strlen( session_root )) + 1); osc_server->send( list_response_address, "/reply", "/nsm/server/list", s ); free( s ); currentSession = ent->fts_parent; //save the directory entry. not the session.nsm entry. } } } // close fts and check for error closing. if(fts_close(ftsp) == -1) FATAL( "fts_close" ); // As marker that all sessions were sent reply with an empty string, which is impossible to conflict with a session name osc_server->send( list_response_address, "/reply", "/nsm/server/list", "" ); return 0; } OSC_HANDLER( open ) { lo_address sender_addr; sender_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); GUIMSG( "Opening session %s", &argv[0]->s ); if ( pending_operation != COMMAND_NONE ) { osc_server->send( sender_addr, "/error", path, ERR_OPERATION_PENDING, "An operation pending." ); return 0; } pending_operation = COMMAND_OPEN; if ( session_path ) { command_all_clients_to_save(); if ( clients_have_errors() ) { osc_server->send( sender_addr, "/error", path, ERR_GENERAL_ERROR, "Some clients could not save" ); pending_operation = COMMAND_NONE; return 0; } // save_session_file(); } char *spath; asprintf( &spath, "%s/%s", session_root, &argv[0]->s ); MESSAGE( "Attempting to open %s", spath ); int err = load_session_file( spath ); if ( ! err ) { MESSAGE( "Loaded" ); osc_server->send( sender_addr, "/reply", path, "Loaded." ); } else { MESSAGE( "Failed" ); const char *m = NULL; switch ( err ) { case ERR_CREATE_FAILED: m = "Could not create session file!"; break; case ERR_SESSION_LOCKED: m = "Session is locked by another process!"; break; case ERR_NO_SUCH_FILE: m = "The named session does not exist."; break; default: m = "Unknown error"; } osc_server->send( sender_addr, "/error", path, err, m ); } free( spath ); MESSAGE( "Done" ); pending_operation = COMMAND_NONE; return 0; } OSC_HANDLER( quit ) { close_session(); handle_signal_clean_exit(0); return 0; } OSC_HANDLER( abort ) { lo_address sender_addr; sender_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); if ( pending_operation != COMMAND_NONE ) { osc_server->send( sender_addr, "/error", path, ERR_OPERATION_PENDING, "An operation pending." ); return 0; } pending_operation = COMMAND_CLOSE; if ( ! session_path ) { osc_server->send( sender_addr, "/error", path, ERR_NO_SESSION_OPEN, "No session to abort." ); goto done; } GUIMSG( "Commanding attached clients to quit." ); close_session(); osc_server->send( sender_addr, "/reply", path, "Aborted." ); MESSAGE( "Done" ); done: pending_operation = COMMAND_NONE; return 0; } OSC_HANDLER( close ) { lo_address sender_addr; sender_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); if ( pending_operation != COMMAND_NONE ) { osc_server->send( sender_addr, "/error", path, ERR_OPERATION_PENDING, "An operation pending." ); return 0; } pending_operation = COMMAND_CLOSE; if ( ! session_path ) { osc_server->send( sender_addr, "/error", path, ERR_NO_SESSION_OPEN, "No session to close." ); goto done; } command_all_clients_to_save(); GUIMSG( "Commanding attached clients to quit." ); close_session(); osc_server->send( sender_addr, "/reply", path, "Closed." ); MESSAGE( "Done" ); done: pending_operation = COMMAND_NONE; return 0; } OSC_HANDLER( broadcast ) { const char *to_path = &argv[0]->s; /* don't allow clients to broadcast NSM commands */ if ( ! strncmp( to_path, "/nsm/", strlen( "/nsm/" ) ) ) return 0; std::list<OSC::OSC_Value> new_args; for ( int i = 1; i < argc; ++i ) { switch ( types[i] ) { case 's': new_args.push_back( OSC::OSC_String( &argv[i]->s ) ); break; case 'i': new_args.push_back( OSC::OSC_Int( argv[i]->i ) ); break; case 'f': new_args.push_back( OSC::OSC_Float( argv[i]->f ) ); break; } } char *sender_url = lo_address_get_url( lo_message_get_source( msg ) ); for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { if ( ! (*i)->addr ) continue; char *url = lo_address_get_url( (*i)->addr ); if ( strcmp( sender_url, url ) ) { osc_server->send( (*i)->addr, to_path, new_args ); } free( url ); } /* also relay to attached GUI so that the broadcast can be * propagated to another NSMD instance */ if ( gui_is_active ) { char *u1 = lo_address_get_url( gui_addr ); if ( strcmp( u1, sender_url ) ) { new_args.push_front( OSC::OSC_String( to_path ) ); osc_server->send( gui_addr, path, new_args ); } free(u1); } free( sender_url ); return 0; } /*********************************/ /* Client Informational Messages */ /*********************************/ OSC_HANDLER( progress ) { Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( c ) { c->progress = argv[0]->f; /* MESSAGE( "%s progress: %i%%", c->name, (int)(c->progress * 100.0f) ); */ if ( gui_is_active ) { osc_server->send( gui_addr, "/nsm/gui/client/progress", c->client_id, (float)c->progress ); } } return 0; } OSC_HANDLER( is_dirty ) { MESSAGE( "Client sends dirty" ); Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( ! c ) return 0; c->dirty = 1; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/dirty", c->client_id, c->dirty ); return 0; } OSC_HANDLER( is_clean ) { MESSAGE( "Client sends clean" ); Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( ! c ) return 0; c->dirty = 0; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/dirty", c->client_id, c->dirty ); return 0; } OSC_HANDLER( gui_is_hidden ) { MESSAGE( "Client sends gui hidden" ); Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( ! c ) return 0; c->gui_visible( false ); if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/gui_visible", c->client_id, c->gui_visible() ); return 0; } OSC_HANDLER( gui_is_shown ) { MESSAGE( "Client sends gui shown" ); Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( ! c ) return 0; c->gui_visible( true ); if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/gui_visible", c->client_id, c->gui_visible() ); return 0; } OSC_HANDLER( message ) { Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( ! c ) return 0; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/message", c->client_id, argv[0]->i, &argv[1]->s ); return 0; } OSC_HANDLER( label ) { Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( ! c ) return 0; if ( strcmp( types, "s" ) ) return -1; c->label( &argv[0]->s ); if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/label", c->client_id, c->label() ); return 0; } /**********************/ /* Response Handlers */ /**********************/ OSC_HANDLER( error ) { Client *c = get_client_by_address( lo_message_get_source( msg ) ); if ( ! c ) { WARNING( "Error from unknown client" ); return 0; } // const char *rpath = &argv[0]->s; int err_code = argv[1]->i; const char *message = &argv[2]->s; c->set_reply( err_code, message ); MESSAGE( "Client \"%s\" replied with error: %s (%i) in %fms", c->name_with_id, message, err_code, c->milliseconds_since_last_command() ); c->pending_command( COMMAND_NONE ); c->status = "error"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); return 0; } OSC_HANDLER( reply ) { Client *c = get_client_by_address( lo_message_get_source( msg ) ); // const char *rpath = &argv[0]->s; const char *message = &argv[1]->s; if ( c ) { c->set_reply( ERR_OK, message ); MESSAGE( "Client \"%s\" replied with: %s in %fms", c->name_with_id, message, c->milliseconds_since_last_command() ); c->pending_command( COMMAND_NONE ); c->status = "ready"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); } else MESSAGE( "Reply from unknown client" ); return 0; } /******************/ /* GUI operations */ /******************/ OSC_HANDLER( stop ) { Client *c = get_client_by_id( &client, &argv[0]->s ); if ( c ) { command_client_to_stop( c ); if ( gui_is_active ) osc_server->send( gui_addr, "/reply", "Client stopped." ); } else { if ( gui_is_active ) osc_server->send( gui_addr, "/error", -10, "No such client." ); } return 0; } OSC_HANDLER( remove ) { Client *c = get_client_by_id( &client, &argv[0]->s ); if ( c ) { if ( c->pid == 0 && ! c->active ) { c->status = "removed"; if ( gui_is_active ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); client.remove( c ); delete c; if ( gui_is_active ) osc_server->send( gui_addr, "/reply", "Client removed." ); } } else { if ( gui_is_active ) osc_server->send( gui_addr, "/error", -10, "No such client." ); } return 0; } OSC_HANDLER( resume ) { Client *c = get_client_by_id( &client, &argv[0]->s ); /* FIXME: return error if no such client? */ if ( c ) { if ( c->pid == 0 && ! c->active ) { if ( ! launch( c->executable_path, c->client_id ) ) { } } } return 0; } OSC_HANDLER( client_save ) { Client *c = get_client_by_id( &client, &argv[0]->s ); /* FIXME: return error if no such client? */ if ( c ) { if ( c->active ) { command_client_to_save( c ); } } return 0; } OSC_HANDLER( client_show_optional_gui ) { Client *c = get_client_by_id( &client, &argv[0]->s ); /* FIXME: return error if no such client? */ if ( c ) { if ( c->active ) { osc_server->send( c->addr, "/nsm/client/show_optional_gui" ); } } return 0; } OSC_HANDLER( client_hide_optional_gui ) { Client *c = get_client_by_id( &client, &argv[0]->s ); /* FIXME: return error if no such client? */ if ( c ) { if ( c->active ) { osc_server->send( c->addr, "/nsm/client/hide_optional_gui" ); } } return 0; } void announce_gui( const char *url, bool is_reply ) { // This is send for a new and empty nsmd as well as already running, headless, ones. // If a GUI connects to an existing server with a running session this will trigger a list of // clients send to the new GUI. MESSAGE ( "A GUI announced to us from the URL %s", url ); gui_addr = lo_address_new_from_url( url ); gui_is_active = true; //global state if ( is_reply ) // the default case. A GUI starts its own nsmd or connects to a running one osc_server->send( gui_addr, "/nsm/gui/gui_announce", "hi" ); else //The server was started directly and instructed to connect to a running GUI. osc_server->send( gui_addr, "/nsm/gui/server_announce", "hi" ); //The session root is not inluced in /nsm/gui/session/name //For the general information we need to send this message: osc_server->send( gui_addr, "/nsm/gui/session/root", session_root ); //Send session name and relative path. If both are empty it signals that no session is currently open, //which is the default state if a GUI started nsmd. //No session_path without session_name. We only need to test for session_name. if ( !session_name || session_name[0] == '\0' ) { MESSAGE( "Informing GUI that no session is running by sending two empty strings" ); osc_server->send( gui_addr, "/nsm/gui/session/name", "", "" ); //Empty string = no current session } else { // Send a list of clients to the newly registered GUI in case there was already a session open // First clients, then session name was original nsmd order. // We keep it that way, the only change is that we made even the attempt dependent on a running session. MESSAGE ( "Informing GUI about %li already running clients", client.size() ); for ( std::list<Client*>::iterator i = client.begin(); i != client.end(); ++i ) { Client *c = *i; osc_server->send( gui_addr, "/nsm/gui/client/new", c->client_id, c->executable_path ); // we send new twice. see announce() comment if ( c->status ) osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); if ( c->is_capable_of( ":optional-gui:" ) ) osc_server->send( gui_addr, "/nsm/gui/client/has_optional_gui", c->client_id ); if ( c->label() ) // could be NULL osc_server->send( gui_addr, "/nsm/gui/client/label", c->client_id, c->label() ); if ( c->active ) osc_server->send( gui_addr, "/nsm/gui/client/new", c->client_id, c->name ); // upgrade to pretty-name } MESSAGE( "Informing GUI about running session name: %s with relative path %s", session_name, session_path + strlen( session_root ) ); osc_server->send( gui_addr, "/nsm/gui/session/name", session_name, session_path + strlen( session_root )); } MESSAGE( "Registration with GUI complete" ); } OSC_HANDLER( gui_announce ) { announce_gui( lo_address_get_url( lo_message_get_source( msg ) ), true ); return 0; } OSC_HANDLER( ping ) { osc_server->send( lo_message_get_source( msg ), "/reply", path ); return 0; } OSC_HANDLER( null ) { WARNING( "Unrecognized message with type signature \"%s\" at path \"%s\"", types, path ); return 0; } static void wait ( long timeout ) { struct signalfd_siginfo fdsi; ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo)); if (s == sizeof(struct signalfd_siginfo)) { if (fdsi.ssi_signo == SIGCHLD) handle_sigchld(); } osc_server->wait( timeout ); purge_dead_clients(); } void handle_signal_clean_exit ( int signal ) { WARNING( "Caught SIGNAL %i. Stopping nsmd.", signal); // We want a clean exit even when things go wrong. close_session(); free( session_root ); free( lockfile_directory ); unlink( daemon_file ); MESSAGE( "Deleted daemon file %s", daemon_file ); free( daemon_file ); exit(0); } int main(int argc, char *argv[]) { signal(SIGINT, handle_signal_clean_exit); signal(SIGTERM, handle_signal_clean_exit); signal(SIGSEGV, handle_signal_clean_exit); sigset_t mask; sigemptyset( &mask ); sigaddset( &mask, SIGCHLD ); sigprocmask(SIG_BLOCK, &mask, NULL ); signal_fd = signalfd( -1, &mask, SFD_NONBLOCK ); /* generate random seed for client ids */ { time_t seconds; time(&seconds); srand( (unsigned int) seconds ); } //Command line parameters char *osc_port = NULL; const char *gui_url = NULL; const char *load_session = NULL; static struct option long_options[] = { { "detach", no_argument, 0, 'd' }, { "session-root", required_argument, 0, 's' }, { "osc-port", required_argument, 0, 'p' }, { "gui-url", required_argument, 0, 'g' }, { "help", no_argument, 0, 'h' }, { "version", no_argument, 0, 'v' }, { "load-session", required_argument, 0, 'l'}, { "quiet", no_argument, 0, 'q'}, //supresses all normal MESSAGE except WARNING and FATAL { 0, 0, 0, 0 } }; int option_index = 0; int c = 0; bool detach = false; while ( ( c = getopt_long_only( argc, argv, "", long_options, &option_index ) ) != -1 ) { switch ( c ) { case 'd': detach = true; break; case 's': { session_root = strdup(optarg); /* get rid of trailing slash */ char *s = rindex(session_root,'/'); if ( s == &session_root[strlen(session_root) - 1] ) *s = '\0'; break; } case 'p': MESSAGE( "Using OSC port %s", optarg ); osc_port = optarg; break; case 'g': MESSAGE( "Going to connect to GUI at: %s", optarg ); gui_url = optarg; break; case 'l': MESSAGE( "Loading existing session file %s", optarg); load_session = optarg; break; case 'v': printf( "%s " VERSION_STRING "\n", argv[0] ); exit(0); break; case 'q': quietMessages = true; //from debug.h break; case 'h': //Print usage message according to POSIX.1-2017 const char *usage = "nsmd - Daemon and server for the 'New Session Manager'\n\n" "Usage:\n" " nsmd\n" " nsmd --help\n" " nsmd --version\n" "\n" "Options:\n" " --help Show this screen\n" " --version Show version\n" " --osc-port portnum OSC port number [Default: provided by system].\n" " --session-root path Base path for sessions [Default: $XDG_DATA_HOME/nsm/].\n" " --load-session name Load existing session [Example: \"My Song\"].\n" " --gui-url url Connect to running legacy-gui [Example: osc.udp://mycomputer.localdomain:38356/].\n" " --detach Detach from console.\n" " --quiet Suppress messages except warnings and errors.\n" "\n\n" "nsmd can be run headless with existing sessions. To create new ones it is recommended to use a GUI\n" "such as nsm-legacy-gui (included) or Agordejo (separate package)\n" ""; puts ( usage ); exit(0); break; } } //Get the XDG runtime directory for lockfiles //https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html //Unlike $XDG_DATA_HOME the runtime env var must be set, usually to /run/user/1000/ struct stat rundir_check; lockfile_directory = getenv( "XDG_RUNTIME_DIR" ); if ( stat( lockfile_directory, &rundir_check ) != 0 && S_ISDIR(rundir_check.st_mode)) { FATAL( "Failed to access $XDG_RUNTIME_DIR directory %s with error: %s", lockfile_directory, strerror( errno ) ); } else { asprintf( &lockfile_directory, "%s/%s", lockfile_directory, "nsm"); //Create the 'nsm' subdirectory. This may fail on it's own. struct stat st_lockfile_dir_mkdir; if ( stat( lockfile_directory, &st_lockfile_dir_mkdir ) ) { if ( mkdir( lockfile_directory, 0771 ) ) { FATAL( "Failed to create lock file directory %s with error: %s", lockfile_directory, strerror( errno ) ); } } MESSAGE( "Using %s for lock-files.", lockfile_directory ); //Now create another subdir for daemons .../nsm/d/ where each daemon has a port number file char * daemon_directory; asprintf( &daemon_directory, "%s/d", lockfile_directory); struct stat st_daemonfile_dir_mkdir; if ( stat( daemon_directory, &st_daemonfile_dir_mkdir ) ) { if ( mkdir( daemon_directory, 0771 ) ) { FATAL( "Failed to create daemon file directory %s with error: %s", daemon_directory, strerror( errno ) ); } } //daemon_file is a global var asprintf( &daemon_file, "%s/%d", daemon_directory, getpid()); free ( daemon_directory ); MESSAGE( "Using %s as daemon file.", daemon_file ); //The actual daemon file will be written below after announcing the session url. } if ( !session_root ) { /* The user gave no specific session directory. We use the default. * The default dir follows the XDG Basedir Specifications: * It is used by looking up environment variables. * https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html * $XDG_DATA_HOME defines the base directory relative to which user-specific data files * should be stored. If $XDG_DATA_HOME is either not set or empty, a default equal to * $HOME/.local/share should be used. * * Up to version 1.5.3 the default dir was ~/NSM Sessions . * If this old directory exists we will use it but write mild warning to the log. * Moving old sessions is left to the user, or an external GUI. */ struct stat st_session_root; //TODO: Valgrind shows a memory leak for the next line. Why? asprintf( &session_root, "%s/%s", getenv( "HOME" ), "NSM Sessions" ); if ( stat( session_root, &st_session_root ) == 0 && S_ISDIR(st_session_root.st_mode)) { WARNING ( "An old session directory was detected in %s. You can continue to use it but it is recommended to move your sessions to $XDG_DATA_HOME/nsm-sessions/. If you don't know where that is simply rename your current session-directory and start nsmd, which will tell you the new directory.", session_root); } else { const char *xdg_data_home = getenv( "XDG_DATA_HOME" ); if ( xdg_data_home ) { //If $XDG_DATA_HOME is explicitly set by the user we assume it to exist. We don't want to recursively create system directories. //If the xdg-dir does not exist yet we FATAL out just below. asprintf( &session_root, "%s/%s", xdg_data_home, "nsm" ); } else { //If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. asprintf( &session_root, "%s/.local/share/nsm", getenv( "HOME" )); } } } struct stat st_session_root_mkdir; if ( stat( session_root, &st_session_root_mkdir ) ) { if ( mkdir( session_root, 0771 ) ) { FATAL( "Failed to create session directory %s with error: %s", session_root, strerror( errno ) ); } } MESSAGE( "Session root is: %s", session_root ); osc_server = new OSC::Endpoint(); if ( osc_server->init( LO_UDP, osc_port ) ) { WARNING( "Failed to create OSC server. Exiting." ); exit( 1 ); } char * url = osc_server->url(); printf( "NSM_URL=%s\n", url ); //Write the URL into the daemon_file that is named after our PID FILE *fpdaemon = fopen( daemon_file, "w" ); if ( !fpdaemon ) { FATAL( "Failed to write daemon file to %s with error: %s", daemon_file, strerror( errno ) ); } fprintf( fpdaemon, "%s\n", url ); MESSAGE( "Created daemon file %s", daemon_file ); fclose( fpdaemon ); free( url ); if ( gui_url ) { //The server was started directly and instructed to connect to a running GUI. announce_gui( gui_url, false ); } /* */ osc_server->add_method( "/nsm/server/announce", "sssiii", OSC_NAME( announce ), NULL, "client_name,capabilities,executable,api_version_major,api_version_minor,client_pid" ); /* response handlers */ osc_server->add_method( "/reply", "ss", OSC_NAME( reply ), NULL, "err_code,msg" ); osc_server->add_method( "/error", "sis", OSC_NAME( error ), NULL, "err_code,msg" ); osc_server->add_method( "/nsm/client/progress", "f", OSC_NAME( progress ), NULL, "progress" ); osc_server->add_method( "/nsm/client/is_dirty", "", OSC_NAME( is_dirty ), NULL, "dirtiness" ); osc_server->add_method( "/nsm/client/is_clean", "", OSC_NAME( is_clean ), NULL, "dirtiness" ); osc_server->add_method( "/nsm/client/message", "is", OSC_NAME( message ), NULL, "message" ); osc_server->add_method( "/nsm/client/gui_is_hidden", "", OSC_NAME( gui_is_hidden ), NULL, "message" ); osc_server->add_method( "/nsm/client/gui_is_shown", "", OSC_NAME( gui_is_shown ), NULL, "message" ); osc_server->add_method( "/nsm/client/label", "s", OSC_NAME( label ), NULL, "message" ); /* */ osc_server->add_method( "/nsm/gui/gui_announce", "", OSC_NAME( gui_announce ), NULL, "" ); osc_server->add_method( "/nsm/gui/client/stop", "s", OSC_NAME( stop ), NULL, "client_id" ); osc_server->add_method( "/nsm/gui/client/remove", "s", OSC_NAME( remove ), NULL, "client_id" ); osc_server->add_method( "/nsm/gui/client/resume", "s", OSC_NAME( resume ), NULL, "client_id" ); osc_server->add_method( "/nsm/gui/client/save", "s", OSC_NAME( client_save ), NULL, "client_id" ); osc_server->add_method( "/nsm/gui/client/show_optional_gui", "s", OSC_NAME( client_show_optional_gui ), NULL, "client_id" ); osc_server->add_method( "/nsm/gui/client/hide_optional_gui", "s", OSC_NAME( client_hide_optional_gui ), NULL, "client_id" ); osc_server->add_method( "/osc/ping", "", OSC_NAME( ping ), NULL, "" ); osc_server->add_method( "/nsm/server/broadcast", NULL, OSC_NAME( broadcast ), NULL, "" ); osc_server->add_method( "/nsm/server/duplicate", "s", OSC_NAME( duplicate ), NULL, "" ); osc_server->add_method( "/nsm/server/abort", "", OSC_NAME( abort ), NULL, "" ); osc_server->add_method( "/nsm/server/list", "", OSC_NAME( list ), NULL, "" ); osc_server->add_method( "/nsm/server/add", "s", OSC_NAME( add ), NULL, "executable_name" ); osc_server->add_method( "/nsm/server/new", "s", OSC_NAME( new ), NULL, "name" ); osc_server->add_method( "/nsm/server/save", "", OSC_NAME( save ), NULL, "" ); osc_server->add_method( "/nsm/server/open", "s", OSC_NAME( open ), NULL, "name" ); osc_server->add_method( "/nsm/server/close", "", OSC_NAME( close ), NULL, "" ); osc_server->add_method( "/nsm/server/quit", "", OSC_NAME( quit ), NULL, "" ); osc_server->add_method( NULL, NULL, OSC_NAME( null ),NULL, "" ); if ( load_session ) { char *spath; asprintf( &spath, "%s/%s", session_root, load_session); // Build the session path. --load-session works with --session-root MESSAGE( "Loading session given by parameter %s", spath); load_session_file( spath ); free ( spath ); } if ( detach ) { MESSAGE( "Detaching from console" ); if ( fork() ) { exit( 0 ); } else { fclose( stdin ); fclose( stdout ); fclose( stderr ); } } /* listen for sigchld signals and process OSC messages forever */ int start_ppid = getppid(); //get parent pid for ( ;; ) { wait( 1000 ); //1000 ms //This still has some corner cases, like a race condition on startup that never gets the real PID, but //we cover the majority of cases at least: if ( start_ppid != getppid() ) { WARNING ( "Our parent PID changed from %d to %d, which indicates a possible GUI crash. The user has no control over the session anymore. Trying to shut down cleanly.", start_ppid, getppid()); handle_signal_clean_exit ( 0 ); } } //Code after here will not be executed if nsmd is stopped with any abort-signal like SIGINT. //Without a signal handler clients will remain active ("zombies") without nsmd as parent. //Therefore exit is handled by handle_signal_clean_exit() return 0; } 07070100000048000081A40000000000000000000000016258A65C0000010D000000000000000000000000000000000000005300000000new-session-manager-1.6.0+git.20220415.0f6719c/src/org.jackaudio.jackpatch.desktop[Desktop Entry] Type=Application Name=JACKpatch Comment=Remember the JACK Audio Connection Kit Graph in NSM Exec=jackpatch Icon=jackpatch Terminal=false StartupNotify=false Version=1.0 Categories=AudioVideo;Audio; X-NSM-Capable=true X-NSM-Exec=jackpatch NoDisplay=true 07070100000049000081A40000000000000000000000016258A65C000001F5000000000000000000000000000000000000005800000000new-session-manager-1.6.0+git.20220415.0f6719c/src/org.jackaudio.nsm-legacy-gui.desktop[Desktop Entry] Type=Application Name=New Session Manager (Legacy GUI) Name[fr]=New Session Manager (ancienne interface) Comment=Audio session manager (Legacy GUI) Comment[de]=Verwaltet Audiositzungen (Alte Oberfläche) Comment[fr]=Gestionnaire de session audio (ancienne interface) Comment[it]=Gestore di sessioni audio (Vecchia GUI) Comment[pt]=Gestor de sessões audio (Antiga GUI) Exec=nsm-legacy-gui Icon=nsm-legacy-gui Terminal=false StartupNotify=false Version=1.0 Categories=AudioVideo;Audio; 0707010000004A000081A40000000000000000000000016258A65C0000010D000000000000000000000000000000000000005300000000new-session-manager-1.6.0+git.20220415.0f6719c/src/org.jackaudio.nsm-proxy.desktop[Desktop Entry] Type=Application Name=NSM-Proxy Comment=Wrapper for executables without direct NSM-Support. Exec=nsm-proxy Icon=nsm-proxy Terminal=false StartupNotify=false Version=1.0 Categories=AudioVideo;Audio; X-NSM-Capable=true X-NSM-Exec=nsm-proxy NoDisplay=true 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!1402 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor