From: Jean Guyader Date: Fri, 19 Jun 2009 13:22:33 +0000 (+0100) Subject: Import Revion 160. X-Git-Url: http://xenbits.xensource.com/gitweb?a=commitdiff_plain;h=afdead1ea331d578f11b09902c5d773a16ff5684;p=xenclient%2Fruby-dbus.git Import Revion 160. https://svn.luon.net/svn/ruby-dbus/trunk/ --- afdead1ea331d578f11b09902c5d773a16ff5684 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5ab7695 --- /dev/null +++ b/COPYING @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..7976b1d --- /dev/null +++ b/ChangeLog @@ -0,0 +1,782 @@ +2007-12-28 09:37 paul + + * ChangeLog, NEWS: Another NEWS and ChangeLog for the 0.2.1 + release. + +2007-12-27 22:26 acornet + + * examples/gdbus/gdbus: gdbus: Don't fail on error. + +2007-12-27 21:57 acornet + + * examples/service/call_service.rb, + examples/service/service_newapi.rb: Variant usage example. + +2007-12-27 21:57 acornet + + * lib/dbus/introspect.rb, lib/dbus/marshall.rb, + lib/dbus/message.rb, lib/dbus/type.rb: Working VARIANT + marshalling (closes: #10). + +2007-12-27 20:51 acornet + + * lib/dbus/marshall.rb: Totally untested VARIANT marshalling code + +2007-12-27 20:11 paul + + * ChangeLog, NEWS: Prepare for the 0.2.1 release; updated the NEWS + and ChangeLog files. + +2007-12-27 20:00 paul + + * examples/gdbus/launch.sh, lib/dbus/matchrule.rb, + lib/dbus/message.rb: Make sure instance variables are + initialized. This surpresses the reported warnings (closes #6). + +2007-12-27 19:50 acornet + + * lib/dbus/marshall.rb: Complete double support. + +2007-12-27 19:47 acornet + + * lib/dbus/marshall.rb: Double marshalling support. Thanks to + patricksissons@gmail.com (closes: #8). + +2007-12-27 19:43 paul + + * lib/dbus/bus.rb, lib/dbus/introspect.rb: Only print stuff if is + set (closes: #3). + +2007-12-27 19:28 acornet + + * lib/dbus/bus.rb, lib/dbus/introspect.rb: Pass errors to receiving + classes. + +2007-12-16 21:34 acornet + + * lib/dbus/marshall.rb, lib/dbus/message.rb: Drake Wilson's fix for + a bug as old as ruby-dbus. + +2007-12-16 21:30 acornet + + * lib/dbus/marshall.rb: Merge Drake Wilson's excellent dict type + support. + +2007-11-19 22:43 acornet + + * examples/gdbus/gdbus: drop old commented-out code. + +2007-11-17 16:40 acornet + + * examples/gdbus/gdbus, lib/dbus/bus.rb, lib/dbus/message.rb: Raise + exception when marshalling a message to + /org/freedesktop/DBus/Local (new in spec). Thanks to sjoerd for + this one. + +2007-11-11 12:39 acornet + + * lib/dbus/marshall.rb: Don't forget to commit the fix. + +2007-11-11 05:17 acornet + + * lib/dbus/marshall.rb: Blank character cosmetics. + +2007-09-22 21:31 acornet + + * examples/gdbus/gdbus, examples/no-introspect/tracker-test.rb: Add + tracker example. + +2007-09-22 21:28 acornet + + * lib/dbus/bus.rb, lib/dbus/introspect.rb, lib/dbus/message.rb: Fix + converting DBUS error to ruby Exception on synchronous calls. + +2007-09-22 20:46 acornet + + * doc/tutorial/src/20.basic_client.page: Fix example in tutorial. + +2007-07-30 08:18 paul + + * lib/dbus/marshall.rb: Fixed unknown variable use in the struct + part of the packet marshaller. + +2007-07-21 11:58 acornet + + * examples/no-introspect, examples/no-introspect/nm-test.rb: Add + example without introspection. + +2007-07-02 19:21 paul + + * NEWS: Prepare for 0.2.0 release. + +2007-06-17 09:42 acornet + + * lib/dbus/introspect.rb: Trivial doc fixes. + +2007-06-17 09:38 acornet + + * lib/dbus/introspect.rb: Trivial doc fixes. + +2007-06-17 09:36 acornet + + * lib/dbus/bus.rb: Trivial doc fixes. + +2007-06-15 16:34 paul + + * lib/dbus.rb, lib/dbus/auth.rb, lib/dbus/bus.rb, + lib/dbus/export.rb, lib/dbus/introspect.rb, + lib/dbus/matchrule.rb, lib/dbus/type.rb: Added documentation for + many classes, modules and methods. + +2007-05-30 18:41 paul + + * NEWS, lib/dbus.rb: Prepare the 0.2.0 release. + +2007-05-26 11:43 acornet + + * lib/dbus/introspect.rb, lib/dbus/marshall.rb, + lib/dbus/matchrule.rb: Parse INT64. Lose a check in on_signal. + +2007-05-26 11:37 acornet + + * examples/gdbus/gdbus: Small error prevention fix. + +2007-05-21 08:26 paul + + * examples/rhythmbox/playpause.rb, + examples/service/call_service.rb, + examples/service/service_newapi.rb, + examples/simple/call_introspect.rb, examples/utils/notify.rb: + Synchronise the syntax in the examples a bit. Small misc fixes. + +2007-05-21 08:17 paul + + * ChangeLog, NEWS: Merged old ChangeLog into NEWS; new ChangeLog + now generated by svn2cl. + +2007-05-19 15:42 acornet + + * examples/gdbus/gdbus: Add a warning in gdbus header. + +2007-05-19 10:04 acornet + + * lib/dbus/marshall.rb: Allow to add two byte integer in packet. + +2007-05-19 09:53 acornet + + * lib/dbus/marshall.rb: Implement integer type parser. Fix typo. + +2007-05-12 19:51 paul + + * NEWS, README: Preparing the 0.1.2 releases: added release to the + NEWS file and updated the README. + +2007-05-12 19:41 paul + + * doc/tutorial/src/20.basic_client.page, + doc/tutorial/src/30.service.page: Tutorial fixes based on review + comments: + * Added a note about the manual introspection requirement. + * Added a remark that explains that when a service name is + requested, + which is denied, an exception is thrown. + +2007-05-12 09:32 acornet + + * doc/tutorial/src/20.basic_client.page: Word the ListNames method + correctly. + +2007-05-10 22:08 acornet + + * doc/tutorial/src/20.basic_client.page: Fix doc mistake. + +2007-05-09 21:46 paul + + * doc/tutorial/src/10.intro.page, + doc/tutorial/src/20.basic_client.page: Applied tutorial fixes + supplied by Bram. Thanks! + +2007-05-06 18:57 paul + + * README: Fixed typo in the README. + +2007-05-06 18:07 paul + + * doc/tutorial/src/00.index.page, doc/tutorial/src/10.intro.page, + doc/tutorial/src/default.css, doc/tutorial/src/index.page: Small + CSS style tweaks and renamed index.page to 00.index.page to + follow the filename convention of the others. + +2007-05-06 16:45 paul + + * doc/tutorial/src/10.intro.page, + doc/tutorial/src/20.basic_client.page, + doc/tutorial/src/30.service.page: Reviewed the tutorial and + reworked some parts. + +2007-05-05 21:27 paul + + * doc/tutorial/src/10.intro.page, + doc/tutorial/src/20.basic_client.page, + doc/tutorial/src/30.service.page, doc/tutorial/src/default.css, + doc/tutorial/src/default.template, doc/tutorial/src/index.page: + Pimped the tutorial: + * Added a new palette to the CSS. + * Add "Ruby D-Bus tutorial" to the title. + * Restructured the pages: added section navigation and more + sections. + * Added some textile style stuff to things such as object names, + object + paths and service names. + * More consistent spelling for: Ruby, D-Bus, buses + * General spellcheck! + * Misc fixes. + +2007-05-05 21:00 acornet + + * doc/tutorial/src/10.intro.page, + doc/tutorial/src/20.basic_client.page, + doc/tutorial/src/30.service.page: Look! I now have super aspell + powers! + +2007-04-25 21:42 paul + + * doc/tutorial/src/default.template: Mention the license in the + tutorial footer. + +2007-04-25 21:32 paul + + * NEWS: Added NEWS file. Should correspond to trac's milestones. + +2007-04-25 21:29 acornet + + * COPYING, lib/dbus.rb, lib/dbus/auth.rb, lib/dbus/bus.rb, + lib/dbus/export.rb, lib/dbus/introspect.rb, lib/dbus/marshall.rb, + lib/dbus/matchrule.rb, lib/dbus/message.rb, lib/dbus/type.rb: + Switch license to LGPL. + +2007-04-25 21:09 paul + + * doc/tutorial/src/10.intro.page, + doc/tutorial/src/20.basic_client.page, + doc/tutorial/src/30.service.page, doc/tutorial/src/default.css, + doc/tutorial/src/default.template, doc/tutorial/src/index.page: + Tutorial style tweaks: template changes, imported old CSS, fixed + some titles and the directoryName. + +2007-04-25 20:44 acornet + + * doc/tutorial/src/30.service.page: Small tutorial refactoring. + +2007-04-25 20:41 paul + + * README: Added feature list. + +2007-04-25 20:36 acornet + + * doc/tutorial/src/30.service.page: Fix code rendering in tutorial. + +2007-04-23 17:53 acornet + + * ChangeLog, lib/dbus/bus.rb: Fix stupid hardcoded string, bug + found by Rudi Cilibrasi. + +2007-04-17 16:15 acornet + + * ChangeLog: Here, have a changelog. + +2007-04-17 16:13 acornet + + * lib/dbus.rb, lib/dbus/auth.rb, lib/dbus/bus.rb, + lib/dbus/export.rb, lib/dbus/introspect.rb, lib/dbus/marshall.rb, + lib/dbus/matchrule.rb, lib/dbus/message.rb, lib/dbus/type.rb: Add + proper licensing terms. + +2007-04-14 08:03 acornet + + * examples/service, examples/simple/service: Move service example + around. + +2007-04-14 08:02 acornet + + * examples/gdbus/gdbus, examples/rhythmbox/playpause.rb, + examples/service, examples/simple/call_introspect.rb, + examples/simple/service/service_newapi.rb, + examples/utils/listnames.rb, examples/utils/notify.rb, + lib/dbus/bus.rb, lib/dbus/introspect.rb, lib/dbus/matchrule.rb, + tests: Move more examples to newer api. Tutorial update. + +2007-04-12 18:16 acornet + + * doc/tutorial/src/10.intro.page, + doc/tutorial/src/20.basic_client.page, + doc/tutorial/src/30.service.page: Tutorial fixes. + +2007-04-11 17:44 acornet + + * doc/tutorial/src/10.intro.page, doc/tutorial/src/10_intro.page, + doc/tutorial/src/20.basic_client.page, + doc/tutorial/src/20_basic_client.page, + doc/tutorial/src/30.service.page, + doc/tutorial/src/30_service.page: Fix names for correct order in + menu. + +2007-04-11 17:43 acornet + + * doc/tutorial/00_intro, doc/tutorial/10_basic_client, + doc/tutorial/20_service, doc/tutorial/src, + doc/tutorial/src/10_intro.page, + doc/tutorial/src/20_basic_client.page, + doc/tutorial/src/30_service.page, doc/tutorial/src/default.css, + doc/tutorial/src/default.template, doc/tutorial/src/index.page: + More tutorial. + +2007-04-10 17:59 acornet + + * doc/tutorial/10_basic_client, doc/tutorial/20_service, + doc/tutorial/30_annoying_client, doc/tutorial/70_signatures, + examples/simple/service/call_service.rb, + examples/simple/service/service_newapi.rb, lib/dbus/bus.rb, + lib/dbus/export.rb, lib/dbus/introspect.rb: More tutorial and a + main loop. + +2007-04-06 16:58 acornet + + * BRAINSTORM, doc/tutorial/10_basic_client, + examples/simple/call_introspect.rb, lib/dbus/bus.rb, + lib/dbus/introspect.rb, lib/dbus/message.rb: More tutorial error + management and fixes + +2007-04-05 17:42 acornet + + * doc, doc/tutorial, doc/tutorial/00_intro, + doc/tutorial/10_basic_client, doc/tutorial/20_service, + doc/tutorial/30_annoying_client, doc/tutorial/70_signatures: + Start of a tutorial + +2007-04-04 18:57 acornet + + * lib/dbus.rb, lib/dbus/auth.rb, lib/dbus/bus.rb: Authenticator ! + +2007-04-03 18:08 acornet + + * examples/gdbus/gdbus, examples/utils/notify.rb, lib/dbus/bus.rb: + Fixes + +2007-04-02 18:28 acornet + + * examples/gdbus/gdbus, examples/simple/call_introspect.rb, + examples/simple/service/call_service.rb, + examples/simple/service/service_newapi.rb, lib/dbus/bus.rb, + lib/dbus/export.rb, lib/dbus/introspect.rb: Pack things up with + new API. + +2007-03-31 10:50 acornet + + * examples/service/service.rb, examples/simple/service, + examples/simple/service.rb, + examples/simple/service/call_service.rb, + examples/simple/service/service_newapi.rb, lib/dbus.rb, + lib/dbus/bus.rb, lib/dbus/export.rb, lib/dbus/introspect.rb: + Implement extremely sexy object definition interface. + See examples/simple/service/* + +2007-03-29 17:02 acornet + + * examples/simple, examples/simple/call_introspect.rb, + examples/simple/service.rb, lib/dbus.rb, lib/dbus/bus.rb, + lib/dbus/export.rb, lib/dbus/introspect.rb, lib/dbus/message.rb: + Now that's a sexy api + +2007-03-28 18:24 acornet + + * examples/gdbus/gdbus, examples/rhythmbox, + examples/rhythmbox/playpause.rb, lib/dbus/bus.rb, + lib/dbus/matchrule.rb: Signals work client side. + +2007-03-28 17:42 acornet + + * examples/gdbus/gdbus, lib/dbus/bus.rb, lib/dbus/introspect.rb, + lib/dbus/matchrule.rb, lib/dbus/message.rb: Some cleanups and + more signal code + +2007-03-27 18:34 acornet + + * examples/gdbus/gdbus, examples/service/call.rb, + examples/service/call_intro.rb, + examples/service/call_intro_async.rb, + examples/service/service.rb, examples/utils/notify.rb, + lib/dbus.rb, lib/dbus/bus.rb, lib/dbus/introspect.rb, + lib/dbus/marshall.rb, lib/dbus/matchrule.rb, lib/dbus/message.rb: + Fairly complete introspection support. + Some signal management. Add funny notify example. + Add matchrule object + +2007-03-26 22:18 acornet + + * examples/service/service.rb, lib/dbus/bus.rb, + lib/dbus/introspect.rb, lib/dbus/message.rb: Now you can register + a service. Service can be introspected. One last word: YAY. + +2007-03-26 18:27 acornet + + * examples/service/call.rb, examples/service/service.rb, + lib/dbus/bus.rb, lib/dbus/introspect.rb: Some more cleanups. + getting closer to have a service running + +2007-03-26 18:01 acornet + + * examples/service/service.rb, lib/dbus.rb, lib/dbus/bus.rb: + Cosmetic fixes. + +2007-03-26 17:55 acornet + + * lib/dbus.rb, lib/dbus/bus.rb, lib/dbus/introspect.rb, + lib/dbus/marshall.rb, lib/dbus/message.rb, lib/dbus/type.rb: + Answer paul's questions. Implement his resquests. Split things + up. + +2007-03-25 00:40 paul + + * lib/dbus.rb: Small style and typo fixes. + +2007-03-25 00:34 paul + + * lib/dbus.rb, lib/dbus/introspect.rb, lib/dbus/type.rb: Added + documentation (mainly to dbus.rb). + +2007-03-24 22:45 acornet + + * lib/dbus.rb: move exception at top of fil + +2007-03-24 14:07 acornet + + * examples/service, examples/service/service.rb, lib/dbus.rb, + lib/dbus/introspect.rb: Some work to allow service creation. + Nothing that actually works yet. + +2007-03-24 10:11 acornet + + * lib/dbus.rb, lib/dbus/introspect.rb: Look! I just learned the + kind_of? method! + +2007-03-23 18:09 acornet + + * lib/dbus/introspect.rb: Quick parsing fix. + +2007-03-23 18:07 acornet + + * BRAINSTORM, lib/dbus/introspect.rb: Start some object exporting + work. + +2007-03-22 23:30 acornet + + * examples/gdbus/gdbus, examples/gdbus/gdbus.glade, lib/dbus.rb, + lib/dbus/introspect.rb, lib/dbus/type.rb: Preliminary ability do + call methods with arguments in gdbus. + Better alignment management. + Missing check in array unmarshalling. + +2007-03-21 22:14 acornet + + * lib/dbus.rb, tests/rhythmboxplaypause.rb: param type handling + fixed. Make rhythmboxplaypause work again. + +2007-03-21 21:42 acornet + + * lib/dbus.rb, tests/rhythmboxplaypause.rb: Small fix in send_sync. + +2007-03-21 20:16 acornet + + * examples/gdbus/gdbus: Slightly more responsive through a + idle_add_priority. Still not satisfying + +2007-03-21 19:53 acornet + + * lib/dbus.rb: Blah, renaming methods... + +2007-03-21 19:53 acornet + + * examples/gdbus/gdbus, lib/dbus.rb: Be more flexible when + extracting messages from the wire. + +2007-03-21 15:21 acornet + + * examples/gdbus/gdbus, lib/dbus.rb: Bugfix in msg parsing and + polling. + + Attempt at making gdbus more responsive on init. + +2007-03-20 16:12 acornet + + * BRAINSTORM: Add some ideas in a plain text file. + +2007-03-19 23:31 acornet + + * examples/gdbus/gdbus, lib/dbus.rb: I think I spotted the problem + that makes gdbus UI not responsive. I have no solution though. + + When there is a lot of dbus messages that stackup, the + while msg = bus.poll_message + is a loop that does not return. Hence we don't go back to the + glib main loop + while there are still buffer on the stack. + +2007-03-19 21:51 paul + + * examples/gdbus/gdbus, tests/rhythmboxplaypause.rb: * Fixed a + small typo. + * Keep the GIO channels in an instance variable otherwise it will + be GC'ed while using it (fixes #2, courtesy of Sjoerd Simons). + +2007-03-19 19:08 acornet + + * examples/gdbus/gdbus, lib/dbus.rb, lib/dbus/introspect.rb: And + commit the API breakage. + +2007-03-19 19:02 acornet + + * examples/utils, examples/utils/listnames.rb: A listnames with a + sexy api. + +2007-03-19 18:43 acornet + + * examples/gdbus/gdbus, lib/dbus/introspect.rb: More beautiful + method defs in gdbus + +2007-03-18 21:22 acornet + + * examples/gdbus/gdbus: Sort the tree a bit. + +2007-03-18 21:15 acornet + + * examples/gdbus/gdbus, examples/gdbus/gdbus.glade, lib/dbus.rb, + lib/dbus/introspect.rb: More gdbus hacking and api fixes. + Fix connect with bigendian architecture support + +2007-03-18 13:34 acornet + + * examples/gdbus/gdbus, examples/gdbus/gdbus.glade, + examples/gdbus/gdbus.glade.old, lib/dbus.rb, + lib/dbus/introspect.rb: Some API polishing. + gDBus is now officially sexy. + +2007-03-18 00:17 acornet + + * examples/gdbus/gdbus, examples/gdbus/gdbus.glade, + examples/gdbus/gdbus.glade.old: Simplify GDbus. Don't make + everything too complex at once. + +2007-03-18 00:13 acornet + + * examples/gdbus/gdbus, lib/dbus.rb, lib/dbus/introspect.rb: Mess + things up a bit :p + +2007-03-17 15:46 acornet + + * examples/gdbus/gdbus, lib/dbus.rb, lib/dbus/introspect.rb: + Introspector fixes. + +2007-03-17 14:51 acornet + + * examples/gdbus/gdbus, examples/gdbus/gdbus.glade: Add window + delete handler. + +2007-03-17 14:40 acornet + + * examples/gdbus/gdbus, examples/gdbus/gdbus.glade, lib/dbus.rb, + lib/dbus/introspect.rb: Some api fixes + +2007-03-17 10:20 acornet + + * examples/gdbus, examples/gdbus/gdbus, examples/gdbus/gdbus.glade, + examples/gdbus/launch.sh, lib/dbus.rb, tests/gdbus: Finally, Glib + IOChannels are easy to cope with. Start a real life example. + +2007-03-17 09:16 acornet + + * lib/dbus/introspect.rb: Add xml validation checks. + +2007-03-17 09:08 acornet + + * lib/dbus.rb, tests/gdbus: Method rename. + +2007-03-17 09:05 acornet + + * contrib, lib/dbus.rb: Use Kristoffer Lundén's hack instead of the + ruby interpreter patch, until the interpreter is fixed. + +2007-03-15 18:31 acornet + + * lib/dbus.rb, lib/dbus/introspect.rb, tests/gdbus: Synchronous + calls. + Use XML and a proxy object for org.freedesktop.DBus methods. + Implement lame dbus stuff viewer. + XML parser fixes. + +2007-03-14 20:47 acornet + + * lib/dbus.rb, lib/dbus/type.rb, tests/listnames.rb: More types. + Few fixes. + +2007-03-13 18:49 acornet + + * lib/dbus.rb, lib/dbus/introspect.rb, lib/dbus/type.rb, + tests/intro.rb, tests/listnames.rb: Lots of stuff: + A ProxyObjectFactory that creates proxy object from XML + definitions. + A signature genrerator (Type#to_s). + Marshaller support for arrays and structs. + +2007-03-12 19:17 acornet + + * lib/dbus.rb, lib/dbus/introspect.rb, tests/intro.rb: Add a basic, + REXML based Introspect data parser. + +2007-03-12 18:39 acornet + + * lib/dbus.rb, tests/listnames.rb: on_return implementation. + +2007-03-11 21:17 acornet + + * lib/dbus.rb, tests/listnames.rb: Fix some alignments. Add + listname.rb example. + +2007-03-11 14:45 acornet + + * lib/dbus.rb, tests/rhythmboxplaypause.rb, tests/unmarshall.rb: + Packet size computation fixes. + + This also comes with another example that actually talks to a + dbus-application. + It asks rhythmbox to play. + +2007-03-10 19:47 paul + + * lib/dbus/type.rb: Fixed typo. + +2007-03-08 11:38 paul + + * README: Fixed the README because it mention 'bindings'. + +2007-03-07 20:53 acornet + + * lib/dbus.rb, tests/connection.rb, tests/ping.rb, + tests/sendsignal.rb: Add message poller. + +2007-03-06 19:09 acornet + + * lib/dbus.rb, tests/connection.rb: Alignment and other fixes. + +2007-03-05 18:37 acornet + + * lib/dbus.rb, tests/conn3.rb, tests/connection.rb: Dirty + request_name implementation. + +2007-03-04 11:07 acornet + + * contrib, contrib/903_abstract_unix_socket.patch, contrib/README: + Add the (hopefully) temporary hack to connect to dbus from ruby. + +2007-03-04 10:19 acornet + + * lib/dbus.rb, tests/unmarshall.rb: Message::unmarshall + implemented. + +2007-03-03 23:00 acornet + + * lib/dbus.rb, tests/conn2.rb, tests/conn3.rb: Basic message + creation. + +2007-03-02 18:27 acornet + + * lib/dbus, lib/dbus.rb, lib/dbus/type.rb, tests/connection.rb, + tests/unmarshall.rb: Some basic stuff: + - Signature parsing + - Packet unmarshalling + - A braindead connection routine without auth + +2007-03-02 13:02 paul + + * README, ext, tests/test000.rb: Removed dbusglue stuff from trunk. + +2007-02-23 10:44 acornet + + * tests/test000.rb: Fix test. + +2007-02-23 10:41 acornet + + * ext/dbus/dbusglue.c: Should be working if it builds. + +2007-02-22 23:28 acornet + + * ext/dbus/bus.c, ext/dbus/dbusglue.c, ext/dbus/message.c, + tests/test000.rb: More stuff, I need to implement + bus.request_name to get a signal working. + +2007-02-22 19:06 acornet + + * ext/dbus/bus.c, ext/dbus/dbusglue.c, tests/test000.rb: More + functions. + +2007-02-22 18:56 acornet + + * ext/dbus/bus.c, ext/dbus/dbusglue.c: Make module names sexier. + +2007-02-22 16:09 acornet + + * ext/dbus/bus.c: bus.c new file for bus methods. + +2007-02-22 15:28 paul + + * ext/dbus/dbus-glue.c, ext/dbus/dbusglue.c, ext/dbus/extconf.rb: + Fixed the source filename (dash not allowed). + +2007-02-22 15:25 acornet + + * ext/dbus/dbus-glue.c: Add some static keywords. + +2007-02-22 15:23 acornet + + * ext/dbus/dbus-glue.c: New messages constructor for signals, + method return and errors. + +2007-02-22 15:15 paul + + * ext/dbus/dbus-glue.c: Fixed some errors to fix building: + * Corrected message types const names. + * Removed unused variables. + +2007-02-22 14:59 acornet + + * ext/dbus/dbus-glue.c: More basic stuff. DBusMessage constructor. + +2007-02-22 14:49 paul + + * COPYING, README: Added README and COPYING files. + +2007-02-22 14:38 paul + + * ext/dbus, ext/dbusglue: Renamed the ext directory. + +2007-02-22 14:37 paul + + * ext/dbusglue, ext/dbusglue/dbus-glue.c, ext/dbusglue/extconf.rb, + ext/dbusglue/pkg-config.rb: Added some preliminary code and some + build stuff. + +2007-02-22 13:22 paul + + * ruby-dbus: Remove redundant directory. + +2007-02-22 13:21 paul + + * examples, ext, lib, ruby-dbus/examples, ruby-dbus/src, + ruby-dbus/tests, setup.rb, tests: Restructure to fit + setup.rb-compatible setup. + +2007-02-22 13:17 paul + + * tags, ., ruby-dbus, ruby-dbus/examples, ruby-dbus/src, + ruby-dbus/tests: Created initial structure in Subversion. + diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..130cd7a --- /dev/null +++ b/NEWS @@ -0,0 +1,31 @@ += Ruby D-Bus NEWS + +== Ruby D-Bus "Thanks for all the fish" 0.2.1 - 2007-12-29 + +More bugfixes, mostly supplied by users supplying us with patches. Thanks! + + * Support for new types added: + - dict (courtesy of Drake Wilson); + - double (courtesy of Patrick Sissons); + - variant. + * Improved exception raise support (courtesy of Sjoerd Simons, + Patrick Sissons). + * Some polish (removed debug output, solved unnecessary warnings). + * Documentation updates, example fixes and updates. + +== Ruby D-Bus "Almost live from DebConf 7" 0.2.0 - 2007-06-02 + +Again a bugfix release, also meant to be the public release +for exploratory purposes. New in 0.2.0: + + * Complete tutorial revamp. + * Relicensed to the LGPL. + +== Ruby D-Bus "Release Often" 0.1.1 - 2007-04-23 + +Bugfix release. Fixes hardcoded string for requesting bus names, +found by Rudi Cilibrasi. + +== Ruby D-Bus "Happy Birthday Paul" 0.1.0 - 2007-04-17 + +First release. Supports most of D-Bus' features. diff --git a/README b/README new file mode 100644 index 0000000..8dd160c --- /dev/null +++ b/README @@ -0,0 +1,53 @@ += Ruby D-Bus README + +Ruby D-Bus provides an implementation of the D-Bus protocol such that the +D-Bus system can be used in the Ruby programming language. + +== Requirements + + * Ruby 1.8 (>= 1.8.6?) + + Optionally, for generating the tutorial: + * Webgen (>= 0.4) + +== Installation + + 1. Decompress the Ruby D-Bus tarball (ruby-dbus-.tar.gz). + 2. Move to top-level directory and type: + + $ ruby setup.rb config + $ ruby setup.rb setup + ($ su) + # ruby setup.rb install + + You can also install files in your favorite directory by + supplying setup.rb some options. Try "ruby setup.rb --help". + +== Feature + +Ruby D-Bus currently supports the following features: + + * Connecting to local buses. + * Accessing remote services, objects and interfaces. + * Invoking methods on remote objects synchronously and asynchronously. + * Catch signals on remote objects and handle them via callbacks. + * Remote object introspection. + * Walking object trees. + * Creating services and registering them on the bus. + * Exporting objects with interfaces on a bus for remote use. + * Rubyish D-Bus object and interface syntax support that automatically + allows for introspection. + * Emitting signals on exported objects. + +== Usage + + See some of the examples in the examples/ subdirectory of the tarball. + Also, check out the included tutorial (in Webgen format) in doc/tutorial/ + or view it online on http://trac.luon.net/data/ruby-dbus/tutorial/. + +== License + + Ruby D-Bus is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by the + Free Software Foundation; either version 2.1 of the License, or (at + your option) any later version. diff --git a/doc/tutorial/src/00.index.page b/doc/tutorial/src/00.index.page new file mode 100644 index 0000000..88c12d6 --- /dev/null +++ b/doc/tutorial/src/00.index.page @@ -0,0 +1,12 @@ +--- +title: Welcome +inMenu: true +directoryName: Start +--- + +h2. Welcome! + +This is the Ruby D-Bus tutorial. It aims to show you the features of Ruby +D-Bus and as you read through the tutorial also how to use them. + +Please proceed to the "Introduction":intro.html. diff --git a/doc/tutorial/src/10.intro.page b/doc/tutorial/src/10.intro.page new file mode 100644 index 0000000..dac42ea --- /dev/null +++ b/doc/tutorial/src/10.intro.page @@ -0,0 +1,127 @@ +--- +title: Introduction +inMenu: true +--- + +This is a tutorial for Ruby D-Bus, a library to access D-Bus facilities of your +system. This chapter has the following sections: + +# "What is D-Bus?":#what-is +# "Definitions":#def +## "Client":#def-client +## "Service":#def-service +## "Object path":#def-obj-path +## "Interface":#def-iface +## "Method":#def-method +## "Signal":#def-signal +## "Message":#def-message +## "Signature":#def-sig + +h2(#what-is). What is D-Bus? + +D-Bus is an RPC(Remote Procedure Call) protocol. A common setup can have +multiple D-Bus daemons running that route procedure calls and signals in +the form of messages. Each of these daemons supports a bus. A bus that +is often used by modern desktop environments, and is available per session, is +called the _session bus_. Another bus that can be available, but in a +system-wide manner, is called the _system bus_. It is used for example by +the "Hardware Abstraction Layer":http://hal.freedesktop.org/ daemon. Note +that theoretically the D-Bus RPC protocol can be used without a system or +session bus. I never came across any actual use of this though. + +At the desktop level, D-Bus allows some components to interact. Typically +if you are writing an application or a personal script that wants to +interact with your web browser, your music player, or that simply wants to +pop-up a desktop notification, D-Bus comes into play. + +At the system level, the Hardware Abstraction Layer is a privileged daemon +that notifies other software of hardware activities. Typically, if you +want to be notified if a CD-ROM has been loaded in, of if you want to +explore hardware, the system daemon comes into play. + +The D-Bus RPC system is as we will see _object oriented_. + +Buses provide access to _services_ provided in turn by running or ready to +run processes. Let me introduce some D-Bus terminology before we discuss +the API of Ruby D-Bus. + +h2(#def). Definitions + +h3(#def-client). Client + +A D-Bus client is a process that connects to a D-Bus. They issue method +calls and register to the bus for signals and events. + +h3(#def-service). Service + +A connected client can export some of its objects and let other clients +call some of its methods. Such clients typically register a special name +like @org.freedesktop.Notifications@, the service name. + +There is slightly different type of service. They are provided by +processes that can be launched by a D-Bus daemon on demand. Once they are +started by D-Bus they register a service name and behave like another +client. + +Note that the buses themselves provide the @org.freedesktop.DBus@ service, +and provide some features through it. + +h3(#def-obj-path). Object path + +An object path is the D-Bus way to specify an object _instance_ address. A +service can provide different object instances to the outside world, so +that external processes can call methods on each of them. An object path +is an address of an instance in a very similar way that the path is an +address of a file on a file system. For example: +@/org/freedesktop/Notification@ is an object path of an object provided by +the @org.freedesktop.Notification@ service + +*Beware*: service names and object paths can, but do _not_ have to be +related! You'll probably encounter a lot of cases though, where the +object path is a slashed version of the dotted service name. + +h3(#def-iface). Interface + +Classically in an object model, classes can implement interfaces. That is, +some method definitions grouped in an interface. This is exactly what a +D-Bus interface is as well. In D-Bus interfaces have names. These names must be +specified on method calls. + +The @org.freedesktop.Notification@ service provides an object instance +called @/org/freedesktop/Notification@. This instance object implements an +interface called @org.freedesktop.Notifications@. It also provides two +special D-Bus specific interfaces: @org.freedesktop.DBus.Introspect@ and +@org.freedesktop.DBus.Properties@. Again, object paths, service names, +and interface names can be related but do not have to be. + +Basically the @org.freedesktop.DBus.Introspect@ has an @Introspect@ method, +that returns XML data describing the @/org/freedesktop/Notification@ object +interfaces. This is used heavily internally by Ruby D-Bus. + +h3(#def-method). Method + +A method is, well, a method in the classical meaning. It's a function that +is called in the context of an object instance. Methods have typed +parameters and return typed return values. + +h3(#def-signal). Signal + +Signals are simplified method calls that do not have a return value. They +do have typed parameters though. + +h3(#def-message). Message + +Method calls, method returns, signals, errors: all are encoded as D-Bus +messages sent over a bus. They are made of a packet header with source and +destination address, a type (method call, method reply, signal) and the +body containing the parameters (for signals and method calls) or the return +values (for a method return message). + +h3(#def-sig). Signature + +Because D-Bus is typed and dynamic, each message comes with a signature that +describes the types of the data that is contained within the message. The +signature is a string with an extremely basic language that only describes +a data type. You will need to have some knowledge of what a signature +looks like if you are setting up a service. If you are just programming a +D-Bus client, you can live without knowing about them. diff --git a/doc/tutorial/src/20.basic_client.page b/doc/tutorial/src/20.basic_client.page new file mode 100644 index 0000000..953f878 --- /dev/null +++ b/doc/tutorial/src/20.basic_client.page @@ -0,0 +1,174 @@ +--- +title: Client Usage +inMenu: true +--- + +This chapter discusses basic client usage and has the following topics: + +# "Using the library":#loading +# "Connecting to a bus":#connecting +# "Performing method calls":#method-call +## "Introspection":#method-call--introspection +# "Calling a method asynchronously":#method-call-async +# "Waiting for a signal":#signal-wait +# "More about introspection":#introspection +## "Walking the object tree":#introspection--tree + +h2(#loading). Using the library + +If you want to use the library, you have to make Ruby load it by issuing: + + require 'dbus' + +That's all! Now we can move on to really using it... + +h2(#connecting). Connecting to a bus + +On a typical system, two buses are running, the system bus and the session +bus. The system bus can be accessed by: + + bus = DBus::SystemBus.instance + +Probably you already have guessed how to access the session bus. This +can be done by: + + bus = DBus::SessionBus.instance + +h2(#method-call). Performing method calls + +Let me continue this example using the session bus. Let's say that I want +to access an object of some client on the session bus. This particular +D-Bus client provides a service called @org.gnome.Rhythmbox@. Let me +access this service: + + rb_service = bus.service("org.gnome.Rhythmbox") + +In this example I access the @org.gnome.Rhythmbox@ service, which is +provided by the application +"Rhythmbox":http://www.gnome.org/projects/rhythmbox/. +OK, I have a service handle now, and I know that it exports the object +"/org/gnome/Rhythmbox/Player". I will trivially access this remote object +using: + + rb_player = rb_service.object("/org/gnome/Rhythmbox/Player") + +h3(#method-call--introspection). Introspection + +Well, that was easy. Let's say that I know that this particular object is +introspectable. In real life most of them are. The @rb_object@ object we +have here is just a handle of a remote object, in general they are called +_proxy objects_, because they are the local handle of a remote object. It +would be nice to be able to make it have methods, and that its methods send +a D-Bus call to remotely execute the actual method in another process. +Well, instating these methods for a _introspectable_ object is trivial: + + rb_player.introspect + +And there you go. Note that not all services or objects can be +introspected, therefore you have to do this manually! Let me remind you +that objects in D-Bus have interfaces and interfaces have methods. Let's +now access these methods: + + rb_player_iface = rb_player["org.gnome.Rhythmbox.Player"] + puts rb_player_iface.getPlayingUri + +As you can see, when you want to call a method on an instance object, you have +to get the correct interface. It is a bit tedious, so we have the following +shortcut that does the same thing as before: + + rb_player.default_iface = "org.gnome.Rhythmbox.Player" + puts rb_player.getPlayingUri + +The @default_iface=@ call specifies the default interface that should be +used when non existing methods are called directly on a proxy object, and +not on one of its interfaces. + +Note that the bus itself has a corresponding introspectable object. You can +access it with @bus.proxy@ method. For example, you can retrieve an array of +exported service names of a bus like this: + + bus.proxy.ListNames[0] + +h2(#method-call-async). Calling a method asynchronously + +D-Bus is _asynchronous_. This means that you do not have to wait for a +reply when you send a message. When you call a remote method that takes a +lot of time to process remotely, you don't want your application to hang, +right? Well the asychronousness exists for this reason. What if you dont' +want to wait for the return value of a method, but still you want to take +some action when you receive it? + +There is a classical method to program this event-driven mechanism. You do +some computation, perform some method call, and at the same time you setup +a callback that will be triggered once you receive a reply. Then you run a +main loop that is responsible to call the callbacks properly. Here is how +you do it: + + rb_player.getPlayingUri do |resp| + puts "The playing URI is #{resp}" + end + puts "See, I'm not waiting!" + loop = DBus::Main.new + loop << bus + loop.run + +This code will print the following: + + See, I'm not waiting! + The playing URI is file:///music/papapingoin.mp3 + +h2(#signal-wait). Waiting for a signal + +Signals are calls from the remote object to your program. As a client, you +set yourself up to receive a signal and handle it with a callback. Then running +the main loop triggers the callback. You can register a callback handler +as allows: + + rb_player.on_signal("elapsedChanged") do |u| + puts u + end + +h2(#introspection). More about introspection + +There are various ways to inspect a remote service. You can simply call +@Introspect()@ and read the XML output. However, in this tutorial I assume +that you want to do it using the Ruby D-Bus API. + +Notice that you can introspect a service, and not only objects: + + rb_service = bus.service("org.gnome.Rhythmbox") + rb_service.introspect + p rb_service.root + +This dumps a tree-like structure that represents multiple object paths. In +this particular case the output is: + + {gnome => {Rhythmbox => {Player => ..fdbe625de {},Shell => ..fdbe6852e {},PlaylistManager => ..fdbe4e340 {}}> + +Read this left to right: the root node is "/", it has one child node "org", +"org" has one child node "gnome", and "gnome" has one child node "Rhythmbox". +Rhythmbox has Tree child nodes "Player", "Shell" and "PlaylistManager". +These three last child nodes have a weird digit that means it has an object +instance. Such object instances are already introspected. + +If the prose wasn't clear, maybe the following ASCII art will help you: + + / + org + gnome + Rhythmbox + Shell (with object) + Player (with object) + PlaylistManager (with object) + +h3(#introspection--tree). Walking the object tree + +You can have an object on any node, i.e. it is not limited to leaves. +You can access a specific node like this: + + rb_player = rb_service.root["org"]["gnome"]["Rhythmbox"]["Player"] + rb_player = rb_service.object("/org/gnome/Rhythmbox/Player") + +The difference between the two is that for the first one, @rb_service@ +needs to have been introspected. Also the obtained @rb_player@ is already +introspected whereas the second @rb_player@ isn't yet. diff --git a/doc/tutorial/src/30.service.page b/doc/tutorial/src/30.service.page new file mode 100644 index 0000000..0aa5a52 --- /dev/null +++ b/doc/tutorial/src/30.service.page @@ -0,0 +1,121 @@ +--- +title: Creating a Service +inMenu: true +--- + +This chapter deals with the opposite side of the basic client usage, namely +the creation of a D-Bus service. It contains the following sections: + +# "Registering a service":#service-reg +# "Exporting an object":#obj-export +## "Using the exported object":#obj-export-use +# "Emitting a signal":#signal-emit + +h2(#service-reg). Registering a service + +Now that you know how to perform D-Bus calls, and how to wait for and +handle signals, you might want to learn how to publish some object and +interface to provide them to the D-Bus world. Here is how you do that. + +As you should already know, D-Bus clients that provide some object to be +called remotely are services. Here is how to allocate a name on a bus: + + bus = DBus.session_bus + service = bus.request_service("org.ruby.service") + +Now this client is know to the outside world as @org.ruby.service@. +Note that this is a request and it _can_ be denied! When it +is denied, an exception (@DBus::NameRequestError@) is thrown. + +h2(#obj-export). Exporting an object + +Now, let's define a class that we want to export: + + class Test < DBus::Object + # Create an interface. + dbus_interface "org.ruby.SampleInterface" do + # Create a hello method in that interface. + dbus_method :hello, "in name:s, in name2:s" do |name, name2| + puts "hello(#{name}, #{name2})" + end + end + end + +As you can see, we define a @Test@ class in which we define a +@org.ruby.SampleInterface@ interface. In this interface, we define a +method. The given code block is the method's implementation. This will be +executed when remote programs performs a D-Bus call. Now the annoying part: +the actual method definition. As you can guess the call + + dbus_method :hello, "in name:s, in name2:s" do ... + +creates a @hello@ method that takes two parameters both of type string. +The _:s_ means "of type string". Let's have a look at some other common +parameter types: + +* _u_ means unsigned integer +* _i_ means integer +* _y_ means byte +* _(ui)_ means a structure having a unsigned integer and a signed one. +* _a_ means array, so that "ai" means array of integers +** _as_ means array of string +** _a(is)_ means array of structures, each having an integer and a string. + +For a full description of the available D-Bus types, please refer to the +"D-Bus specification":http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-signatures. + +Now that the class has been defined, we can instantiate an object +and export it as follows: + + exported_obj = Test.new("/org/ruby/MyInstance") + service.export(exported_obj) + +This piece of code above instantiates a @Test@ object with a D-Bus object +path. This object is reachable from the outside world after +@service.export(exported_obj)@ is called. + +h3(#obj-export-use). Using the exported object + +Now, let's consider another program that will access our newly created service: + + ruby_service = bus.service("org.ruby.service") + obj = ruby_service.object("/org/ruby/MyInstance") + obj.introspect + obj.default_iface = "org.ruby.SampleInterface" + obj.hello("giligiligiligili", "haaaaaaa") + +As you can see, the object we defined earlier is automatically introspectable. +See also "Basic Client Usage":basic_client.html. + +h2(#signal-emit). Emitting a signal + +Let's add some example method so you can see how to return a value to the +caller and let's also define another example interface that has a signal. + + class Test2 < DBus::Object + # Create an interface + dbus_interface "org.ruby.SampleInterface" do + # Create a hello method in the interface: + dbus_method :hello, "in name:s, in name2:s" do |name, name2| + puts "hello(#{name}, #{name2})" + end + # Define a signal in the interface: + dbus_signal :SomethingJustHappened, "toto:s, tutu:u" + end + + dbus_interface "org.ruby.AnotherInterface" do + dbus_method :ThatsALongMethodNameIThink, "in name:s, out ret:s" do |name| + ["So your name is #{name}"] + end + end + end + +Triggering the signal is a easy as calling a method, but then this time on +a local (exported) object and not on a remote/proxy object: + + exported_obj.SomethingJustHappened("blah", 1) + +Note that the @ThatsALongMethodNameIThink@ method is returning a single +value to the caller. Notice that you always have to return an array. If +you want to return multiple values, just have an array with multiple +values. diff --git a/doc/tutorial/src/default.css b/doc/tutorial/src/default.css new file mode 100644 index 0000000..db95f63 --- /dev/null +++ b/doc/tutorial/src/default.css @@ -0,0 +1,129 @@ + a { text-decoration: none; } + a:link { color: #2E5F84; background-color: inherit; } + a:visited { color: #4084B8; background-color: inherit; } + a:hover, + a:active { + color: #4084B8; + background-color: inherit; + text-decoration: underline; + } + + #content { + width: 50em; + margin: 0pt auto; + } + + #header { + padding: 2pt; + color: inherit; + background-color: #C61C18; + border-top: thin solid #891D1B; + border-left: thin solid #891D1B; + border-right: thin solid #891D1B; + } + + #header h1 { + margin: 5pt; + color: #FFFFFF; + background-color: inherit; + } + + #footer { + margin-top: 5pt; + border-top: thin solid #891D1B; + } + + #body { text-align: justify; } + + .bar { + clear: both; + padding: 2pt; + text-align: center; + font-size: 83%; + border: thin solid #891D1B; + color: inherit; + background-color: #F07F7D; + } + .bar a:link { color: #FFFFFF; background-color: inherit; } + .bar a:visited { color: #FFFFFF; background-color: inherit; } + + .left, .right { + padding: 0pt 1em; + } + + .left { + float: left; + text-align: left; + } + + .right { + float: right; + text-align: right; + } + + .log { table-layout: fixed; width: 100%; } + .log tr.header .day { width: 6em; } + + code { color: inherit; background-color: #F0E7E7; } + + pre { + font-size: 90%; + overflow: hidden; + padding-left: 10pt; + border: thin solid #F0B4B4; + color: inherit; + background-color: #F0DDDD; + } + + pre code { color: inherit; background-color: #F0DDDD; } + + h2, h3 { + color: #1D3B52; + background-color: inherit; + border-bottom: thin dashed #1D3B52; + } + + table th { color: inherit; background-color: #B4D2E8; } + + /* styling the menu */ + + #menu { + float: right; + width: 15em; + margin: 10pt 0pt 5pt 10pt; + font-size: 83%; + border: thin solid #2E5F84; + color: inherit; + background-color: #B4D2E8; + } + + #menu a { + text-decoration: none; + } + + #menu a:hover { + text-decoration: underline; + } + + #menu li.webgen-menu-item-selected { + font-weight: bold; + } + + #menu ul { + list-style-type: none; + padding-left: 15pt; /* first ul has some padding left & right */ + padding-right: 5pt; + } + + #menu li { + padding: 3pt 0pt; /* inter-item padding */ + color: #2E5F84; + background-color: inherit; + } + + #menu li > ul { + font-weight: normal; + padding-top: 3pt; /* padding in front of first item */ + padding-left: 7pt; /* indentation */ + padding-right: 0pt; + } diff --git a/doc/tutorial/src/default.template b/doc/tutorial/src/default.template new file mode 100644 index 0000000..cf02887 --- /dev/null +++ b/doc/tutorial/src/default.template @@ -0,0 +1,46 @@ +--- content, html + + + + + The Ruby D-Bus Tutorial - {title: } + + + + + + + + + +
+ + +
+ Location: {breadcrumbTrail: } + Language: {langbar: } +
+
+ + + +
+ {block: } +
+ + +
+ + diff --git a/examples/gdbus/gdbus b/examples/gdbus/gdbus new file mode 100644 index 0000000..4f7b46d --- /dev/null +++ b/examples/gdbus/gdbus @@ -0,0 +1,255 @@ +#!/usr/bin/ruby +# +# This is a quite complex example using internal lower level API. +# Not a good starting point, but might be usefull if you want to do tricky +# stuff. +# -- Arnaud + +require 'dbus' +require 'libglade2' + +$enable_system = false + +class MethodCallWindow + def initialize(pwindow, intf, meth) + @intf, @meth = intf, meth + @entries = Array.new + @dialog = Gtk::Dialog.new(meth.name, pwindow, + Gtk::Dialog::MODAL | Gtk::Dialog::NO_SEPARATOR, + [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], + [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]) + + @meth.params.each do |param| + shbox = Gtk::HBox.new(true, 0) + label = Gtk::Label.new("#{param[0]} (#{param[1]})") + input = Gtk::Entry.new + @entries << input + shbox.pack_start(label, true, true, 0) + shbox.pack_start(input, true, true, 0) + @dialog.vbox.pack_start(shbox, true, true, 0) + @dialog.vbox.show_all + end + end + + def run + on_ok if @dialog.run == Gtk::Dialog::RESPONSE_OK + @dialog.destroy + end + + def on_ok + bus = @intf.object.bus + m = DBus::Message.new(DBus::Message::METHOD_CALL) + m.path = @intf.object.path + m.interface = @intf.name + m.destination = @intf.object.destination + m.member = @meth.name + m.sender = bus.unique_name + @meth.params.each_with_index do |param, idx| + entry = @entries[idx] + data = nil + case param[1] + when "u", "i" + data = entry.text.to_i + when "s" + data = entry.text + when /^a/ + begin + data = eval(entry.text) + rescue + puts "Incorrect data: #{data}" + end + end + m.add_param(param[1], data) + end + bus.on_return(m) do |retm| + if retm.is_a?(DBus::Error) + puts "Error: #{retm.inspect}" + else + puts "Method #{m.member} returns: #{retm.params.inspect}" + end + end + bus.send(m.marshall) + end +end + +class DBusUI + def initialize + @glade = GladeXML.new("gdbus.glade") { |h| method(h) } # This block is like + # black magic :) + @sessiontreeview = @glade.get_widget("sessiontreeview") + setup_treeview_renderer(@sessiontreeview, 'D-Bus Objects') + @sessiontreeview.selection.signal_connect("changed") do |selection| + on_treeview_selection_changed(selection) + end + + @systemtreeview = @glade.get_widget("systemtreeview") + setup_treeview_renderer(@systemtreeview, 'D-Bus Objects') + @systemtreeview.selection.signal_connect("changed") do |selection| + on_treeview_selection_changed(selection) + end + + @methsigtreeview = @glade.get_widget("methsigtreeview") + # ierk + setup_methodview_renderer(@methsigtreeview) + + @window = @glade.get_widget("window1") + @window.show_all + start_buses + end + + def beautify_method(meth) + # Damn, this need to be rewritten :p + s = meth.name + "(" + if meth.kind_of?(DBus::Method) + s += (meth.params.collect { |a| "in #{a[0]}:#{a[1]}" } + + meth.rets.collect { |a| "out #{a[0]}:#{a[1]}" }).join(", ") + elsif meth.kind_of?(DBus::Signal) + s += (meth.params.collect { |a| "in #{a[0]}:#{a[1]}" }).join(", ") + end + s += ")" + s + end + + def on_treeview_selection_changed(selection) + selected = selection.selected + model = Gtk::ListStore.new(String, String, DBus::Method, + DBus::ProxyObjectInterface) + @methsigtreeview.model = model + if selected + if intf = selected[1] + intf.methods.keys.sort.each do |mi| + m = intf.methods[mi] + subiter = model.append + subiter[0] = beautify_method(m) + subiter[1] = "M" + subiter[2] = m + subiter[3] = intf + end + intf.signals.keys.sort.each do |mi| + m = intf.signals[mi] + subiter = model.append + subiter[0] = beautify_method(m) + subiter[1] = "S" + subiter[2] = m + subiter[3] = intf + end + end + end + end + + def on_method_activated(view, path, column) + name = view.model.get_iter(path)[0] + puts "Clicked on: #{name.inspect}" + type = view.model.get_iter(path)[1] + intf = view.model.get_iter(path)[2] + if type == "M" + method = view.model.get_iter(path)[2] + intf = view.model.get_iter(path)[3] + MethodCallWindow.new(@window, intf, method).run + elsif type == "S" + signal = view.model.get_iter(path)[2] + intf = view.model.get_iter(path)[3] + mr = DBus::MatchRule.new.from_signal(intf, signal) + puts "*** Registering matchrule: #{mr.to_s} ***" + intf.object.bus.add_match(mr) do |sig| + puts "Got #{sig.member}(#{sig.params.join(',')})" + end + end + end + + def on_sessiontreeview_row_activated(view, path, column) + name = view.model.get_iter(path)[0] + puts "Clicked on: #{name.inspect}" + intf = view.model.get_iter(path)[1] + end + + def on_window_delete_event(window, event) + Gtk.main_quit + end + + def setup_methodview_renderer(treeview) + renderer = Gtk::CellRendererText.new + col_offset = treeview.insert_column(-1, "T", renderer, 'text' => 1) + col_offset = treeview.insert_column(-1, "Name", renderer, 'text' => 0) + column = treeview.get_column(col_offset - 1) + column.clickable = true + end + + def setup_treeview_renderer(treeview, str) + renderer = Gtk::CellRendererText.new + col_offset = treeview.insert_column(-1, str, renderer, 'text' => 0) + column = treeview.get_column(col_offset - 1) + column.clickable = true + end + + def process_input(bus) + # THIS is the bad ass loop + # we should return to the glib main loop from time to time. Anyone with a + # proper way to handle it ? + bus.update_buffer + bus.messages.each do |msg| + bus.process(msg) + end + end + + def start_buses + # call glibize to get dbus messages from the glib mainloop + DBus::SessionBus.instance.glibize + DBus::SystemBus.instance.glibize if $enable_system + + DBus::SessionBus.instance.proxy.ListNames do |msg, names| + fill_treeview(DBus::SessionBus.instance, @sessiontreeview, names) + end + if $enable_system + DBus::SystemBus.instance.proxy.ListNames do |msg, names| + fill_treeview(DBus::SystemBus.instance, @systemtreeview, names) + end + end + end + + def walk_node(model, iter, node) + node.each_pair do |key, val| + subiter = model.append(iter) + subiter[0] = key + walk_node(model, subiter, val) + end + unless node.object.nil? + node.object.interfaces.sort.each do |ifname| + subiter = model.append(iter) + subiter[0] = ifname + subiter[1] = node.object[ifname] + end + end + end + + def introspect_services(model, bus) + el = @introspect_array.shift + if not el =~ /^:/ + iter = model.append(nil) + iter[0] = el + puts "introspecting: #{el}" + begin + service = bus.service(el).introspect + walk_node(model, iter, service.root) + rescue Exception => e + puts "DBus Error:" + puts e.backtrace.join("\n") + end + end + + not @introspect_array.empty? + end + + def fill_treeview(bus, treeview, array) + model = Gtk::TreeStore.new(String, DBus::ProxyObjectInterface) + treeview.model = model + @introspect_array = array.sort + Gtk::idle_add { introspect_services(model, bus) } + end + + def main + Gtk.main + end +end + +DBusUI.new.main diff --git a/examples/gdbus/gdbus.glade b/examples/gdbus/gdbus.glade new file mode 100644 index 0000000..109e386 --- /dev/null +++ b/examples/gdbus/gdbus.glade @@ -0,0 +1,184 @@ + + + + + + + True + GD-Bus + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 500 + 400 + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + + True + True + + + + True + True + True + True + GTK_POS_TOP + False + False + + + + True + True + GTK_POLICY_ALWAYS + GTK_POLICY_ALWAYS + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + True + False + False + True + False + False + False + + + + + + False + True + + + + + + True + Session + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + + True + True + GTK_POLICY_ALWAYS + GTK_POLICY_ALWAYS + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + True + False + False + True + False + False + False + + + + + False + True + + + + + + True + System + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + True + False + + + + + + True + True + GTK_POLICY_ALWAYS + GTK_POLICY_ALWAYS + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + True + False + False + True + False + False + False + + + + + + True + False + + + + + + + diff --git a/examples/gdbus/launch.sh b/examples/gdbus/launch.sh new file mode 100755 index 0000000..f6c61f7 --- /dev/null +++ b/examples/gdbus/launch.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +# for the lazy typer +ruby -w -I ../../lib gdbus diff --git a/examples/no-introspect/nm-test.rb b/examples/no-introspect/nm-test.rb new file mode 100644 index 0000000..c83e0fe --- /dev/null +++ b/examples/no-introspect/nm-test.rb @@ -0,0 +1,16 @@ +#!/usr/bin/ruby +# +# Trivial network interface lister using NetworkManager. +# NetworkManager does not support introspection, so the api is not that sexy. + +require 'dbus' + +bus = DBus::SystemBus.instance + +nm_service = bus.service("org.freedesktop.NetworkManager") +nm_manager = nm_service.object("/org/freedesktop/NetworkManager") +poi = DBus::ProxyObjectInterface.new(nm_manager, "org.freedesktop.NetworkManager") +poi.define_method("getDevices", "") +p poi.getDevices + + diff --git a/examples/no-introspect/tracker-test.rb b/examples/no-introspect/tracker-test.rb new file mode 100644 index 0000000..b9b1b62 --- /dev/null +++ b/examples/no-introspect/tracker-test.rb @@ -0,0 +1,16 @@ +#!/usr/bin/ruby +# +# Trivial network interface lister using NetworkManager. +# NetworkManager does not support introspection, so the api is not that sexy. + +require 'dbus' + +bus = DBus::SessionBus.instance + +tracker_service = bus.service("org.freedesktop.Tracker") +tracker_manager = tracker_service.object("/org/freedesktop/tracker") +poi = DBus::ProxyObjectInterface.new(tracker_manager, "org.freedesktop.Tracker.Files") +poi.define_method("GetMetadataForFilesInFolder", "in live_query_id:i, in uri:s, in fields:as, out values:aas") +p poi.GetMetadataForFilesInFolder(-1, ENV['HOME'] + "/Desktop", ["File:Name", "File:Size"]) + + diff --git a/examples/rhythmbox/playpause.rb b/examples/rhythmbox/playpause.rb new file mode 100644 index 0000000..8368ae1 --- /dev/null +++ b/examples/rhythmbox/playpause.rb @@ -0,0 +1,25 @@ +#!/usr/bin/ruby + +require 'dbus' +bus = DBus::SessionBus.instance +# get a rb object +proxy = bus.introspect("org.gnome.Rhythmbox", "/org/gnome/Rhythmbox/Player") +proxyi = proxy["org.gnome.Rhythmbox.Player"] + +# register for signals + +mr = DBus::MatchRule.new +mr.type = "signal" +mr.interface = "org.gnome.Rhythmbox.Player" +mr.path = "/org/gnome/Rhythmbox/Player" +bus.add_match(mr) do |msg, first_param| + print msg.member + " " + puts first_param +end + +proxyi.playPause(true) + +main = DBus::Main.new +main << bus +main.run + diff --git a/examples/service/call_service.rb b/examples/service/call_service.rb new file mode 100644 index 0000000..4907bf8 --- /dev/null +++ b/examples/service/call_service.rb @@ -0,0 +1,25 @@ +#!/usr/bin/ruby + +require "dbus" + +session_bus = DBus::SessionBus.instance + +ruby_srv = session_bus.service("org.ruby.service") + +# Get the object from this service +player = ruby_srv.object("/org/ruby/MyInstance") + +# Introspect it +puts player.introspect +player.default_iface = "org.ruby.SampleInterface" +player.test_variant(["s", "coucou"]) +player.on_signal("SomethingJustHappened") do |u, v| + puts "SomethingJustHappened: #{u} #{v}" +end +player.hello("8=======D", "(_._)") +p player["org.ruby.AnotherInterface"].Reverse("Hello world!") + +main = DBus::Main.new +main << session_bus +main.run + diff --git a/examples/service/service_newapi.rb b/examples/service/service_newapi.rb new file mode 100644 index 0000000..9c49735 --- /dev/null +++ b/examples/service/service_newapi.rb @@ -0,0 +1,51 @@ +#!/usr/bin/ruby + +require 'dbus' +require 'thread' +Thread.abort_on_exception = true + +class Test < DBus::Object + # Create an interface aggregating all upcoming dbus_method defines. + dbus_interface "org.ruby.SampleInterface" do + dbus_method :hello, "in name:s, in name2:s" do |name, name2| + puts "hello(#{name}, #{name2})" + end + + dbus_method :test_variant, "in stuff:v" do |variant| + p variant + end + + dbus_signal :SomethingJustHappened, "toto:s, tutu:u" + end + + dbus_interface "org.ruby.AnotherInterface" do + dbus_method :ThatsALongMethodNameIThink do + puts "ThatsALongMethodNameIThink" + end + dbus_method :Reverse, "in instr:s, out outstr:s" do |instr| + outstr = instr.split(//).reverse.join + puts "got: #{instr}, replying: #{outstr}" + [outstr] + end + end +end + +bus = DBus::SessionBus.instance +service = bus.request_service("org.ruby.service") +myobj = Test.new("/org/ruby/MyInstance") +service.export(myobj) + +Thread.new do + i = 0 + loop do + # Signal emission + myobj.SomethingJustHappened("hey", i += 1) + sleep(0.5) + end +end + +puts "listening" +main = DBus::Main.new +main << bus +main.run + diff --git a/examples/simple/call_introspect.rb b/examples/simple/call_introspect.rb new file mode 100644 index 0000000..375ed8b --- /dev/null +++ b/examples/simple/call_introspect.rb @@ -0,0 +1,34 @@ +#!/usr/bin/ruby + +require "dbus" + +session_bus = DBus::SessionBus.instance + +# Get the Rhythmbox service +rhythmbox = session_bus.service("org.gnome.Rhythmbox") + +# Get the object from this service +player = rhythmbox.object("/org/gnome/Rhythmbox/Player") + +# Introspect it +player.introspect +if player.has_iface? "org.gnome.Rhythmbox.Player" + puts "We have Rhythmbox Player interface" +end + +player_with_iface = player["org.gnome.Rhythmbox.Player"] +p player_with_iface.getPlayingUri + +# Maybe support default_iface=(iface_str) on an ProxyObject, so +# that this is possible? +player.default_iface = "org.gnome.Rhythmbox.Player" +puts "default_iface test:" +p player.getPlayingUri +player.on_signal("elapsedChanged") do |u| + puts "elapsedChanged: #{u}" +end + +main = DBus::Main.new +main << session_bus +main.run + diff --git a/examples/utils/listnames.rb b/examples/utils/listnames.rb new file mode 100644 index 0000000..6b46f9a --- /dev/null +++ b/examples/utils/listnames.rb @@ -0,0 +1,11 @@ +#!/usr/bin/ruby + +require 'dbus' + +d = if ARGV.member?("--system") + DBus::SystemBus.instance +else + DBus::SessionBus.instance +end +d.proxy.ListNames[0].each{ |n| puts "\t#{n}" } + diff --git a/examples/utils/notify.rb b/examples/utils/notify.rb new file mode 100644 index 0000000..81391b9 --- /dev/null +++ b/examples/utils/notify.rb @@ -0,0 +1,19 @@ +#!/usr/bin/ruby + +require 'dbus' + +if ARGV.size < 2 + puts "Usage:" + puts "notify.rb \"title\" \"body\"" + exit +end + +d = DBus::SessionBus.instance +o = d.service("org.freedesktop.Notifications").object("/org/freedesktop/Notifications") +o.introspect + +i = o["org.freedesktop.Notifications"] + +i.Notify('notify.rb', 0, 'info', ARGV[0], ARGV[1], [], {}, 2000) do |ret, param| +end + diff --git a/lib/dbus.rb b/lib/dbus.rb new file mode 100644 index 0000000..f1b8de4 --- /dev/null +++ b/lib/dbus.rb @@ -0,0 +1,82 @@ +# dbus.rb - Module containing the low-level D-Bus implementation +# +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +require 'dbus/type' +require 'dbus/introspect' +require 'dbus/export' +require 'dbus/bus.rb' +require 'dbus/marshall' +require 'dbus/message' +require 'dbus/matchrule' +require 'dbus/auth' + +require 'socket' +require 'thread' + +# = D-Bus main module +# +# Module containing all the D-Bus modules and classes. +module DBus + # Default socket name for the system bus. + SystemSocketName = "unix:path=/var/run/dbus/system_bus_socket" + + # Byte signifying big endianness. + BIG_END = ?B + # Byte signifying little endianness. + LIL_END = ?l + + # Byte signifying the host's endianness. + HOST_END = if [0x01020304].pack("L").unpack("V")[0] == 0x01020304 + LIL_END + else + BIG_END + end + + # General exceptions. + + # Exception raised when an invalid packet is encountered. + class InvalidPacketException < Exception + end + + # Exception raised when there is a problem with a type (may be unknown or + # mismatch). + class TypeException < Exception + end + + # Exception raised when an unmarshalled buffer is truncated and + # incomplete. + class IncompleteBufferException < Exception + end + + # Exception raised when an interface is not implemented. + class InterfaceNotImplemented < Exception + end + + # Exception raised when an method is not found in the interface. + class MethodNotInInterface < Exception + end + + # Exception raised when a method has not been implemented (yet). + class MethodNotImplemented < Exception + end + + # Exception raised when a method is invoked with invalid + # parameters (wrong number or type). + class InvalidParameters < Exception + end + + # Exception raised when an invalid method name is used. + class InvalidMethodName < Exception + end + + # Exception raised when invalid introspection data is parsed/used. + class InvalidIntrospectionData < Exception + end +end # module DBus diff --git a/lib/dbus/auth.rb b/lib/dbus/auth.rb new file mode 100644 index 0000000..42a3243 --- /dev/null +++ b/lib/dbus/auth.rb @@ -0,0 +1,156 @@ +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +module DBus + # Exception raised when authentication fails somehow. + class AuthenticationFailed < Exception + end + + # = General class for authentication. + class Authenticator + # Returns the name of the authenticator. + def name + self.class.to_s.upcase.sub(/.*::/, "") + end + end + + # = External authentication class + # + # Class for 'external' type authentication. + class External < Authenticator + # Performs the authentication. + def authenticate + # Take the user id (eg integer 1000) make a string out of it "1000", take + # each character and determin hex value "1" => 0x31, "0" => 0x30. You + # obtain for "1000" => 31303030 This is what the server is expecting. + # Why? I dunno. How did I come to that conclusion? by looking at rbus + # code. I have no idea how he found that out. + return Process.uid.to_s.split(//).collect { |a| "%x" % a[0] }.join + end + end + + # Note: this following stuff is tested with External authenticator only! + + # = Authentication client class. + # + # Class tha performs the actional authentication. + class Client + # Create a new authentication client. + def initialize(socket) + @socket = socket + @state = nil + @auth_list = [External] + end + + # Start the authentication process. + def authenticate + @socket.write(0.chr) + next_authenticator + @state = :Starting + while @state != :Authenticated + r = next_state + return r if not r + end + true + end + + ########## + private + ########## + + # Send an authentication method _meth_ with arguments _args_ to the + # server. + def send(meth, *args) + o = ([meth] + args).join(" ") + @socket.write(o + "\r\n") + end + + # Try authentication using the next authenticator. + def next_authenticator + raise AuthenticationFailed if @auth_list.size == 0 + @authenticator = @auth_list.shift.new + send("AUTH", @authenticator.name, @authenticator.authenticate) + end + + + # Read data (a buffer) from the bus until CR LF is encountered. + # Return the buffer without the CR LF characters. + def next_msg + @socket.readline.chomp.split(" ") + end + + # Try to reach the next state based on the current state. + def next_state + msg = next_msg + if @state == :Starting + case msg[0] + when "CONTINUE" + @state = :WaitingForData + when "OK" + @state = :WaitingForOk + end + end + case @state + when :WaitingForData + case msg[0] + when "DATA" + chall = msg[1] + resp, chall = @authenticator.data(chall) + case resp + when :AuthContinue + send("DATA", chall) + @state = :WaitingForData + when :AuthOk + send("DATA", chall) + @state = :WaitingForOk + when :AuthError + send("ERROR") + @state = :WaitingForData + end + when "REJECTED" + next_authenticator + @state = :WaitingForData + when "ERROR" + send("CANCEL") + @state = :WaitingForReject + when "OK" + send("BEGIN") + @state = :Authenticated + else + send("ERROR") + @state = :WaitingForData + end + when :WaitingForOk + case msg[0] + when "OK" + send("BEGIN") + @state = :Authenticated + when "REJECT" + next_authenticator + @state = :WaitingForData + when "DATA", "ERROR" + send("CANCEL") + @state = :WaitingForReject + else + send("ERROR") + @state = :WaitingForOk + end + when :WaitingForReject + case msg[0] + when "REJECT" + next_authenticator + @state = :WaitingForOk + else + @socket.close + return false + end + end + return true + end # def next_state + end # class Client +end # module D-Bus diff --git a/lib/dbus/bus.rb b/lib/dbus/bus.rb new file mode 100644 index 0000000..c528d7a --- /dev/null +++ b/lib/dbus/bus.rb @@ -0,0 +1,690 @@ +# dbus.rb - Module containing the low-level D-Bus implementation +# +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +require 'socket' +require 'thread' +require 'singleton' + +# = D-Bus main module +# +# Module containing all the D-Bus modules and classes. +module DBus + # This represents a remote service. It should not be instancied directly + # Use Bus::service() + class Service + # The service name. + attr_reader :name + # The bus the service is running on. + attr_reader :bus + # The service root (FIXME). + attr_reader :root + + # Create a new service with a given _name_ on a given _bus_. + def initialize(name, bus) + @name, @bus = name, bus + @root = Node.new("/") + end + + # Determine whether the serice name already exists. + def exists? + bus.proxy.ListName.member?(@name) + end + + # Perform an introspection on all the objects on the service + # (starting recursively from the root). + def introspect + if block_given? + raise NotImplementedError + else + rec_introspect(@root, "/") + end + self + end + + # Retrieves an object at the given _path_. + def object(path) + node = get_node(path, true) + if node.object.nil? + node.object = ProxyObject.new(@bus, @name, path) + end + node.object + end + + # Export an object _obj_ (an DBus::Object subclass instance). + def export(obj) + obj.service = self + get_node(obj.path, true).object = obj + end + + # Get the object node corresponding to the given _path_. if _create_ is + # true, the the nodes in the path are created if they do not already exist. + def get_node(path, create = false) + n = @root + path.sub(/^\//, "").split("/").each do |elem| + if not n[elem] + if not create + return nil + else + n[elem] = Node.new(elem) + end + end + n = n[elem] + end + if n.nil? + puts "Warning, unknown object #{path}" if $DEBUG + end + n + end + + ######### + private + ######### + + # Perform a recursive retrospection on the given current _node_ + # on the given _path_. + def rec_introspect(node, path) + xml = bus.introspect_data(@name, path) + intfs, subnodes = IntrospectXMLParser.new(xml).parse + subnodes.each do |nodename| + subnode = node[nodename] = Node.new(nodename) + if path == "/" + subpath = "/" + nodename + else + subpath = path + "/" + nodename + end + rec_introspect(subnode, subpath) + end + if intfs.size > 0 + node.object = ProxyObjectFactory.new(xml, @bus, @name, path).build + end + end + end + + # = Object path node class + # + # Class representing a node on an object path. + class Node < Hash + # The D-Bus object contained by the node. + attr_accessor :object + # The name of the node. + attr_reader :name + + # Create a new node with a given _name_. + def initialize(name) + @name = name + @object = nil + end + + # Return an XML string representation of the node. + def to_xml + xml = ' + +' + self.each_pair do |k, v| + xml += "" + end + if @object + @object.intfs.each_pair do |k, v| + xml += %{\n} + v.methods.each_value { |m| xml += m.to_xml } + v.signals.each_value { |m| xml += m.to_xml } + xml +="\n" + end + end + xml += '' + xml + end + + # Return inspect information of the node. + def inspect + # Need something here + "" + end + + # Return instance inspect information, used by Node#inspect. + def sub_inspect + s = "" + if not @object.nil? + s += "%x " % @object.object_id + end + s + "{" + keys.collect { |k| "#{k} => #{self[k].sub_inspect}" }.join(",") + "}" + end + end # class Inspect + + # FIXME: rename Connection to Bus? + + # D-Bus main connection class + # + # Main class that maintains a connection to a bus and can handle incoming + # and outgoing messages. + class Connection + # The unique name (by specification) of the message. + attr_reader :unique_name + # The socket that is used to connect with the bus. + attr_reader :socket + + # Create a new connection to the bus for a given connect _path_. _path_ + # format is described in the D-Bus specification: + # http://dbus.freedesktop.org/doc/dbus-specification.html#addresses + # and is something like: + # "transport1:key1=value1,key2=value2;transport2:key1=value1,key2=value2" + # e.g. "unix:path=/tmp/dbus-test" + # + # Current implementation of ruby-dbus supports only a single server + # address and only "unix:path=...,guid=..." and + # "unix:abstract=...,guid=..." forms + def initialize(path) + @path = path + @unique_name = nil + @buffer = "" + @method_call_replies = Hash.new + @method_call_msgs = Hash.new + @signal_matchrules = Array.new + @proxy = nil + # FIXME: can be TCP or any stream + @socket = Socket.new(Socket::Constants::PF_UNIX, + Socket::Constants::SOCK_STREAM, 0) + @object_root = Node.new("/") + end + + # Connect to the bus and initialize the connection. + def connect + parse_session_string + if @transport == "unix" and @type == "abstract" + if HOST_END == LIL_END + sockaddr = "\1\0\0#{@unix_abstract}" + else + sockaddr = "\0\1\0#{@unix_abstract}" + end + elsif @transport == "unix" and @type == "path" + sockaddr = Socket.pack_sockaddr_un(@unix) + end + @socket.connect(sockaddr) + init_connection + end + + # Send the buffer _buf_ to the bus using Connection#writel. + def send(buf) + @socket.write(buf) + end + + # Tell a bus to register itself on the glib main loop + def glibize + require 'glib2' + # Circumvent a ruby-glib bug + @channels ||= Array.new + + gio = GLib::IOChannel.new(@socket.fileno) + @channels << gio + gio.add_watch(GLib::IOChannel::IN) do |c, ch| + update_buffer + messages.each do |msg| + process(msg) + end + true + end + end + + # FIXME: describe the following names, flags and constants. + # See DBus spec for definition + NAME_FLAG_ALLOW_REPLACEMENT = 0x1 + NAME_FLAG_REPLACE_EXISTING = 0x2 + NAME_FLAG_DO_NOT_QUEUE = 0x4 + + REQUEST_NAME_REPLY_PRIMARY_OWNER = 0x1 + REQUEST_NAME_REPLY_IN_QUEUE = 0x2 + REQUEST_NAME_REPLY_EXISTS = 0x3 + REQUEST_NAME_REPLY_ALREADY_OWNER = 0x4 + + DBUSXMLINTRO = ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +' + + def introspect_data(dest, path) + m = DBus::Message.new(DBus::Message::METHOD_CALL) + m.path = path + m.interface = "org.freedesktop.DBus.Introspectable" + m.destination = dest + m.member = "Introspect" + m.sender = unique_name + if not block_given? + # introspect in synchronous ! + send_sync(m) do |rmsg| + if rmsg.is_a?(Error) + raise rmsg + else + return rmsg.params[0] + end + end + else + send(m.marshall) + on_return(m) do |rmsg| + if rmsg.is_a?(Error) + yield rmsg + else + yield rmsg.params[0] + end + end + end + nil + end + + # Issues a call to the org.freedesktop.DBus.Introspectable.Introspect method + # _dest_ is the service and _path_ the object path you want to introspect + # If a code block is given, the introspect call in asynchronous. If not + # data is returned + # + # FIXME: link to ProxyObject data definition + # The returned object is a ProxyObject that has methods you can call to + # issue somme METHOD_CALL messages, and to setup to receive METHOD_RETURN + def introspect(dest, path) + if not block_given? + # introspect in synchronous ! + data = introspect_data(dest, path) + pof = DBus::ProxyObjectFactory.new(data, self, dest, path) + return pof.build + else + introspect_data(dest, path) do |data| + yield(DBus::ProxyObjectFactory.new(data, self, dest, path).build) + end + end + end + + # Exception raised when a service name is requested that is not available. + class NameRequestError < Exception + end + + # Attempt to request a service _name_. + def request_service(name) + r = proxy.RequestName(name, NAME_FLAG_REPLACE_EXISTING) + raise NameRequestError if r[0] != REQUEST_NAME_REPLY_PRIMARY_OWNER + @service = Service.new(name, self) + @service + end + + # Set up a ProxyObject for the bus itself, since the bus is introspectable. + # Returns the object. + def proxy + if @proxy == nil + path = "/org/freedesktop/DBus" + dest = "org.freedesktop.DBus" + pof = DBus::ProxyObjectFactory.new(DBUSXMLINTRO, self, dest, path) + @proxy = pof.build["org.freedesktop.DBus"] + end + @proxy + end + + # Fill (append) the buffer from data that might be available on the + # socket. + def update_buffer + @buffer += @socket.read_nonblock(MSG_BUF_SIZE) + end + + # Get one message from the bus and remove it from the buffer. + # Return the message. + def pop_message + ret = nil + begin + ret, size = Message.new.unmarshall_buffer(@buffer) + @buffer.slice!(0, size) + rescue IncompleteBufferException => e + # fall through, let ret be null + end + ret + end + + # Retrieve all the messages that are currently in the buffer. + def messages + ret = Array.new + while msg = pop_message + ret << msg + end + ret + end + + # The buffer size for messages. + MSG_BUF_SIZE = 4096 + + # Update the buffer and retrieve all messages using Connection#messages. + # Return the messages. + def poll_messages + ret = nil + r, d, d = IO.select([@socket], nil, nil, 0) + if r and r.size > 0 + update_buffer + end + messages + end + + # Wait for a message to arrive. Return it once it is available. + def wait_for_message + ret = pop_message + while ret == nil + r, d, d = IO.select([@socket]) + if r and r[0] == @socket + update_buffer + ret = pop_message + end + end + ret + end + + # Send a message _m_ on to the bus. This is done synchronously, thus + # the call will block until a reply message arrives. + def send_sync(m, &retc) # :yields: reply/return message + send(m.marshall) + @method_call_msgs[m.serial] = m + @method_call_replies[m.serial] = retc + + retm = wait_for_message + process(retm) + until [DBus::Message::ERROR, + DBus::Message::METHOD_RETURN].include?(retm.message_type) and + retm.reply_serial == m.serial + retm = wait_for_message + process(retm) + end + end + + # Specify a code block that has to be executed when a reply for + # message _m_ is received. + def on_return(m, &retc) + # Have a better exception here + if m.message_type != Message::METHOD_CALL + raise "on_return should only get method_calls" + end + @method_call_msgs[m.serial] = m + @method_call_replies[m.serial] = retc + end + + # Asks bus to send us messages matching mr, and execute slot when + # received + def add_match(mr, &slot) + # check this is a signal. + @signal_matchrules << [mr, slot] + self.proxy.AddMatch(mr.to_s) + end + + # Process a message _m_ based on its type. + # method call:: FIXME... + # method call return value:: FIXME... + # signal:: FIXME... + # error:: FIXME... + def process(m) + case m.message_type + when Message::ERROR, Message::METHOD_RETURN + raise InvalidPacketException if m.reply_serial == nil + mcs = @method_call_replies[m.reply_serial] + if not mcs + puts "no return code for #{mcs.inspect} (#{m.inspect})" if $DEBUG + else + if m.message_type == Message::ERROR + mcs.call(Error.new(m)) + else + mcs.call(m) + end + @method_call_replies.delete(m.reply_serial) + @method_call_msgs.delete(m.reply_serial) + end + when DBus::Message::METHOD_CALL + if m.path == "/org/freedesktop/DBus" + puts "Got method call on /org/freedesktop/DBus" if $DEBUG + end + # handle introspectable as an exception: + if m.interface == "org.freedesktop.DBus.Introspectable" and + m.member == "Introspect" + reply = Message.new(Message::METHOD_RETURN).reply_to(m) + reply.sender = @unique_name + node = @service.get_node(m.path) + raise NotImplementedError if not node + reply.sender = @unique_name + reply.add_param(Type::STRING, @service.get_node(m.path).to_xml) + send(reply.marshall) + else + node = @service.get_node(m.path) + return if node.nil? + obj = node.object + return if obj.nil? + obj.dispatch(m) if obj + end + when DBus::Message::SIGNAL + @signal_matchrules.each do |elem| + mr, slot = elem + if mr.match(m) + slot.call(m) + return + end + end + else + puts "Unknown message type: #{m.message_type}" if $DEBUG + end + end + + # Retrieves the service with the given _name_. + def service(name) + # The service might not exist at this time so we cannot really check + # anything + Service.new(name, self) + end + alias :[] :service + + # Emit a signal event for the given _service_, object _obj_, interface + # _intf_ and signal _sig_ with arguments _args_. + def emit(service, obj, intf, sig, *args) + m = Message.new(DBus::Message::SIGNAL) + m.path = obj.path + m.interface = intf.name + m.member = sig.name + m.sender = service.name + i = 0 + sig.params.each do |par| + m.add_param(par[1], args[i]) + i += 1 + end + send(m.marshall) + end + + ########################################################################### + private + + # Send a hello messages to the bus to let it know we are here. + def send_hello + m = Message.new(DBus::Message::METHOD_CALL) + m.path = "/org/freedesktop/DBus" + m.destination = "org.freedesktop.DBus" + m.interface = "org.freedesktop.DBus" + m.member = "Hello" + send_sync(m) do |rmsg| + @unique_name = rmsg.destination + puts "Got hello reply. Our unique_name is #{@unique_name}" if $DEBUG + end + end + + # Parse the session string (socket address). + def parse_session_string + path_parsed = /^([^:]*):([^;]*)$/.match(@path) + @transport = path_parsed[1] + adr = path_parsed[2] + if @transport == "unix" + adr.split(",").each do |eqstr| + idx, val = eqstr.split("=") + case idx + when "path" + @type = idx + @unix = val + when "abstract" + @type = idx + @unix_abstract = val + when "guid" + @guid = val + end + end + end + end + + # Initialize the connection to the bus. + def init_connection + @client = Client.new(@socket) + @client.authenticate + # TODO: code some real stuff here + #writel("AUTH EXTERNAL 31303030") + #s = readl + # parse OK ? + #writel("BEGIN") + end + end # class Connection + + # = D-Bus session bus class + # + # The session bus is a session specific bus (mostly for desktop use). + # This is a singleton class. + class SessionBus < Connection + include Singleton + + # Get the the default session bus. + def initialize + super(ENV["DBUS_SESSION_BUS_ADDRESS"]) + connect + send_hello + end + end + + # = D-Bus system bus class + # + # The system bus is a system-wide bus mostly used for global or + # system usages. This is a singleton class. + class SystemBus < Connection + include Singleton + + # Get the default system bus. + def initialize + super(SystemSocketName) + connect + send_hello + end + end + + # FIXME: we should get rid of these + + def DBus.system_bus + SystemBus.instance + end + + def DBus.session_bus + SessionBus.instance + end + + # = Main event loop class. + # + # Class that takes care of handling message and signal events + # asynchronously. *Note:* This is a native implement and therefore does + # not integrate with a graphical widget set main loop. + class Main + # Create a new main event loop. + def initialize + @buses = Hash.new + end + + # Add a _bus_ to the list of buses to watch for events. + def <<(bus) + @buses[bus.socket] = bus + end + + # Run the main loop. This is a blocking call! + def run + loop do + ready, dum, dum = IO.select(@buses.keys) + ready.each do |socket| + b = @buses[socket] + b.update_buffer + while m = b.pop_message + b.process(m) + end + end + end + end + end # class Main +end # module DBus diff --git a/lib/dbus/export.rb b/lib/dbus/export.rb new file mode 100644 index 0000000..f51716e --- /dev/null +++ b/lib/dbus/export.rb @@ -0,0 +1,123 @@ +# dbus/introspection.rb - module containing a low-level D-Bus introspection implementation +# +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +require 'thread' + +module DBus + # Exception raised when an interface cannot be found in an object. + class InterfaceNotInObject < Exception + end + + # Exception raised when a method cannot be found in an inferface. + class MethodNotInInterface < Exception + end + + # Method raised when a method returns an invalid return type. + class InvalidReturnType < Exception + end + + # Exported object type + # = Exportable D-Bus object class + # + # Objects that are going to be exported by a D-Bus service + # should inherit from this class. + class Object + # The path of the object. + attr_reader :path + # The interfaces that the object supports. + attr_reader :intfs + # The service that the object is exported by. + attr_writer :service + + @@intfs = Hash.new + @@cur_intf = nil + @@intfs_mutex = Mutex.new + + # Create a new object with a given _path_. + def initialize(path) + @path = path + @intfs = @@intfs.dup + @service = nil + end + + # State that the object implements the given _intf_. + def implements(intf) + @intfs[intf.name] = intf + end + + # Dispatch a message _msg_. + def dispatch(msg) + case msg.message_type + when Message::METHOD_CALL + if not @intfs[msg.interface] + raise InterfaceNotInObject, msg.interface + end + meth = @intfs[msg.interface].methods[msg.member.to_sym] + raise MethodNotInInterface if not meth + methname = Object.make_method_name(msg.interface, msg.member) + retdata = method(methname).call(*msg.params).to_a + + reply = Message.new.reply_to(msg) + meth.rets.zip(retdata).each do |rsig, rdata| + reply.add_param(rsig[1], rdata) + end + @service.bus.send(reply.marshall) + end + end + + # Select (and create) the interface that the following defined methods + # belong to. + def self.dbus_interface(s) + @@intfs_mutex.synchronize do + @@cur_intf = @@intfs[s] = Interface.new(s) + yield + @@cur_intf = nil + end + end + + # Dummy undefined interface class. + class UndefinedInterface + end + + # Defines an exportable method on the object with the given name _sym_, + # _prototype_ and the code in a block. + def self.dbus_method(sym, protoype = "", &block) + raise UndefinedInterface if @@cur_intf.nil? + @@cur_intf.define(Method.new(sym.to_s).from_prototype(protoype)) + define_method(Object.make_method_name(@@cur_intf.name, sym.to_s), &block) + end + + # Emits a signal from the object with the given _interface_, signal + # _sig_ and arguments _args_. + def emit(intf, sig, *args) + @service.bus.emit(@service, self, intf, sig, *args) + end + + # Defines a signal for the object with a given name _sym_ and _prototype_. + def self.dbus_signal(sym, protoype = "") + raise UndefinedInterface if @@cur_intf.nil? + cur_intf = @@cur_intf + signal = Signal.new(sym.to_s).from_prototype(protoype) + cur_intf.define(Signal.new(sym.to_s).from_prototype(protoype)) + define_method(sym.to_s) do |*args| + emit(cur_intf, signal, *args) + end + end + + #################################################################### + private + + # Helper method that returns a method name generated from the interface + # name _intfname_ and method name _methname_. + def self.make_method_name(intfname, methname) + "#{intfname}%%#{methname}" + end + end # class Object +end # module DBus diff --git a/lib/dbus/introspect.rb b/lib/dbus/introspect.rb new file mode 100644 index 0000000..bbb09a4 --- /dev/null +++ b/lib/dbus/introspect.rb @@ -0,0 +1,509 @@ +# dbus/introspection.rb - module containing a low-level D-Bus introspection implementation +# +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +require 'rexml/document' + +module DBus + # Regular expressions that should match all method names. + MethodSignalRE = /^[A-Za-z][A-Za-z0-9_]*$/ + # Regular expressions that should match all interface names. + InterfaceElementRE = /^[A-Za-z][A-Za-z0-9_]*$/ + + # Exception raised when an unknown signal is used. + class UnknownSignal < Exception + end + + # Exception raised when an invalid class definition is encountered. + class InvalidClassDefinition < Exception + end + + # = D-Bus interface class + # + # This class is the interface descriptor. In most cases, the Introspect() + # method call instanciates and configures this class for us. + # + # It also is the local definition of interface exported by the program. + class Interface + # The name of the interface. + attr_reader :name + # The methods that are part of the interface. + attr_reader :methods + # The signals that are part of the interface. + attr_reader :signals + + # Creates a new interface with a given _name_. + def initialize(name) + validate_name(name) + @name = name + @methods, @signals = Hash.new, Hash.new + end + + # Validates a service _name_. + def validate_name(name) + raise InvalidIntrospectionData if name.size > 255 + raise InvalidIntrospectionData if name =~ /^\./ or name =~ /\.$/ + raise InvalidIntrospectionData if name =~ /\.\./ + raise InvalidIntrospectionData if not name =~ /\./ + name.split(".").each do |element| + raise InvalidIntrospectionData if not element =~ InterfaceElementRE + end + end + + # Helper method for defining a method _m_. + def define(m) + if m.kind_of?(Method) + @methods[m.name.to_sym] = m + elsif m.kind_of?(Signal) + @signals[m.name.to_sym] = m + end + end + alias :<< :define + + # Defines a method with name _id_ and a given _prototype_ in the + # interface. + def define_method(id, prototype) + m = Method.new(id) + m.from_prototype(prototype) + define(m) + end + end # class Interface + + # = D-Bus interface element class + # + # This is a generic class for entities that are part of the interface + # such as methods and signals. + class InterfaceElement + # The name of the interface element. + attr_reader :name + # The parameters of the interface element + attr_reader :params + + # Validates element _name_. + def validate_name(name) + if (not name =~ MethodSignalRE) or (name.size > 255) + raise InvalidMethodName + end + end + + # Creates a new element with the given _name_. + def initialize(name) + validate_name(name.to_s) + @name = name + @params = Array.new + end + + # Adds a parameter _param_. + def add_param(param) + @params << param + end + end # class InterfaceElement + + # = D-Bus interface method class + # + # This is a class representing methods that are part of an interface. + class Method < InterfaceElement + # The list of return values for the method. + attr_reader :rets + + # Creates a new method interface element with the given _name_. + def initialize(name) + super(name) + @rets = Array.new + end + + # Add a return value _ret_. + def add_return(ret) + @rets << ret + end + + # Add parameter types by parsing the given _prototype_. + def from_prototype(prototype) + prototype.split(/, */).each do |arg| + arg = arg.split(" ") + raise InvalidClassDefinition if arg.size != 2 + dir, arg = arg + if arg =~ /:/ + arg = arg.split(":") + name, sig = arg + else + sig = arg + end + case dir + when "in" + add_param([name, sig]) + when "out" + add_return([name, sig]) + end + end + self + end + + # Return an XML string representation of the method interface elment. + def to_xml + xml = %{\n} + @params.each do |param| + name = param[0] ? %{name="#{param[0]}" } : "" + xml += %{\n} + end + @rets.each do |param| + name = param[0] ? %{name="#{param[0]}" } : "" + xml += %{\n} + end + xml += %{\n} + xml + end + end # class Method + + # = D-Bus interface signal class + # + # This is a class representing signals that are part of an interface. + class Signal < InterfaceElement + # Add parameter types based on the given _prototype_. + def from_prototype(prototype) + prototype.split(/, */).each do |arg| + if arg =~ /:/ + arg = arg.split(":") + name, sig = arg + else + sig = arg + end + add_param([name, sig]) + end + self + end + + # Return an XML string representation of the signal interface elment. + def to_xml + xml = %{\n} + @params.each do |param| + name = param[0] ? %{name="#{param[0]}" } : "" + xml += %{\n} + end + xml += %{\n} + xml + end + end # class Signal + + # = D-Bus introspect XML parser class + # + # This class parses introspection XML of an object and constructs a tree + # of Node, Interface, Method, Signal instances. + class IntrospectXMLParser + # Creates a new parser for XML data in string _xml_. + def initialize(xml) + @xml = xml + end + + # Recursively parses the subnodes, constructing the tree. + def parse_subnodes + subnodes = Array.new + t = Time.now + d = REXML::Document.new(@xml) + d.elements.each("node/node") do |e| + subnodes << e.attributes["name"] + end + subnodes + end + + # Parses the XML, constructing the tree. + def parse + ret = Array.new + subnodes = Array.new + t = Time.now + d = REXML::Document.new(@xml) + d.elements.each("node/node") do |e| + subnodes << e.attributes["name"] + end + d.elements.each("node/interface") do |e| + i = Interface.new(e.attributes["name"]) + e.elements.each("method") do |me| + m = Method.new(me.attributes["name"]) + parse_methsig(me, m) + i << m + end + e.elements.each("signal") do |se| + s = Signal.new(se.attributes["name"]) + parse_methsig(se, s) + i << s + end + ret << i + end + d = Time.now - t + if d > 2 + puts "Some XML took more that two secs to parse. Optimize me!" if $DEBUG + end + [ret, subnodes] + end + + ###################################################################### + private + + # Parses a method signature XML element _e_ and initialises + # method/signal _m_. + def parse_methsig(e, m) + e.elements.each("arg") do |ae| + name = ae.attributes["name"] + dir = ae.attributes["direction"] + sig = ae.attributes["type"] + if m.is_a?(DBus::Signal) + m.add_param([name, sig]) + elsif m.is_a?(DBus::Method) + case dir + when "in" + m.add_param([name, sig]) + when "out" + m.add_return([name, sig]) + end + else + raise NotImplementedError, dir + end + end + end + end # class IntrospectXMLParser + + # = D-Bus proxy object interface class + # + # A class similar to the normal Interface used as a proxy for remote + # object interfaces. + class ProxyObjectInterface + # The proxied methods contained in the interface. + attr_accessor :methods + # The proxied signals contained in the interface. + attr_accessor :signals + # The proxy object to which this interface belongs. + attr_reader :object + # The name of the interface. + attr_reader :name + + # Creates a new proxy interface for the given proxy _object_ + # and the given _name_. + def initialize(object, name) + @object, @name = object, name + @methods, @signals = Hash.new, Hash.new + end + + # Returns the string representation of the interface (the name). + def to_str + @name + end + + # Returns the singleton class of the interface. + def singleton_class + (class << self ; self ; end) + end + + # FIXME + def check_for_eval(s) + raise RuntimeException, "invalid internal data" if not s.to_s =~ /^[A-Za-z0-9_]*$/ + end + + # FIXME + def check_for_quoted_eval(s) + raise RuntimeException, "invalid internal data" if not s.to_s =~ /^[^"]+$/ + end + + # Defines a method on the interface from the descriptor _m_. + def define_method_from_descriptor(m) + check_for_eval(m.name) + check_for_quoted_eval(@name) + methdef = "def #{m.name}(" + methdef += (0..(m.params.size - 1)).to_a.collect { |n| + "arg#{n}" + }.join(", ") + methdef += %{) + msg = Message.new(Message::METHOD_CALL) + msg.path = @object.path + msg.interface = "#{@name}" + msg.destination = @object.destination + msg.member = "#{m.name}" + msg.sender = @object.bus.unique_name + } + idx = 0 + m.params.each do |npar| + paramname, par = npar + check_for_quoted_eval(par) + + # This is the signature validity check + Type::Parser.new(par).parse + + methdef += %{ + msg.add_param("#{par}", arg#{idx}) + } + idx += 1 + end + methdef += " + ret = nil + if block_given? + @object.bus.on_return(msg) do |rmsg| + if rmsg.is_a?(Error) + yield(rmsg) + else + yield(rmsg, *rmsg.params) + end + end + @object.bus.send(msg.marshall) + else + @object.bus.send_sync(msg) do |rmsg| + if rmsg.is_a?(Error) + raise rmsg + else + ret = rmsg.params + end + end + end + ret + end + " + singleton_class.class_eval(methdef) + @methods[m.name] = m + end + + # Defines a signal from the descriptor _s_. + def define_signal_from_descriptor(s) + @signals[s.name] = s + end + + # Defines a signal or method based on the descriptor _m_. + def define(m) + if m.kind_of?(Method) + define_method_from_descriptor(m) + elsif m.kind_of?(Signal) + define_signal_from_descriptor(m) + end + end + + # Defines a proxied method on the interface. + def define_method(methodname, prototype) + m = Method.new(methodname) + m.from_prototype(prototype) + define(m) + end + + # Registers a handler (code block) for a signal with _name_ arriving + # over the given _bus_. + def on_signal(bus, name, &block) + mr = DBus::MatchRule.new.from_signal(self, name) + bus.add_match(mr) { |msg| block.call(*msg.params) } + end + end # class ProxyObjectInterface + + # D-Bus proxy object class + # + # Class representing a remote object in an external application. + # Typically, calling a method on an instance of a ProxyObject sends a message + # over the bus so that the method is executed remotely on the correctponding + # object. + class ProxyObject + # The subnodes of the object in the tree. + attr_accessor :subnodes + # Flag determining whether the object has been introspected. + attr_accessor :introspected + # The (remote) destination of the object. + attr_reader :destination + # The path to the object. + attr_reader :path + # The bus the object is reachable via. + attr_reader :bus + # The default interface of the object. + attr_accessor :default_iface + + # Creates a new proxy object living on the given _bus_ at destination _dest_ + # on the given _path_. + def initialize(bus, dest, path) + @bus, @destination, @path = bus, dest, path + @interfaces = Hash.new + @subnodes = Array.new + end + + # Returns the interfaces of the object. + def interfaces + @interfaces.keys + end + + # Retrieves an interface of the proxy object (ProxyObjectInterface instance). + def [](intfname) + @interfaces[intfname] + end + + # Maps the given interface name _intfname_ to the given interface _intf. + def []=(intfname, intf) + @interfaces[intfname] = intf + end + + # Introspects the remote object. Allows you to find and select + # interfaces on the object. + def introspect + # Synchronous call here. + xml = @bus.introspect_data(@destination, @path) + ProxyObjectFactory.introspect_into(self, xml) + xml + end + + # Returns whether the object has an interface with the given _name_. + def has_iface?(name) + raise "Cannot call has_iface? is not introspected" if not @introspected + @interfaces.member?(name) + end + + # Registers a handler, the code block, for a signal with the given _name_. + def on_signal(name, &block) + if @default_iface and has_iface?(@default_iface) + @interfaces[@default_iface].on_signal(@bus, name, &block) + else + raise NoMethodError + end + end + + #################################################### + private + + # Handles all unkown methods, mostly to route method calls to the + # default interface. + def method_missing(name, *args) + if @default_iface and has_iface?(@default_iface) + @interfaces[@default_iface].method(name).call(*args) + else + raise NoMethodError + end + end + end # class ProxyObject + + # = D-Bus proxy object factory class + # + # Class that generates and sets up a proxy object based on introspection data. + class ProxyObjectFactory + # Creates a new proxy object factory for the given introspection XML _xml_, + # _bus_, destination _dest_, and _path_. + def initialize(xml, bus, dest, path) + @xml, @bus, @path, @dest = xml, bus, path, dest + end + + # Investigates the sub-nodes of the proxy object _po_ based on the + # introspection XML data _xml_ and sets them up recursively. + def ProxyObjectFactory.introspect_into(po, xml) + intfs, po.subnodes = IntrospectXMLParser.new(xml).parse + intfs.each do |i| + poi = ProxyObjectInterface.new(po, i.name) + i.methods.each_value { |m| poi.define(m) } + i.signals.each_value { |s| poi.define(s) } + po[i.name] = poi + end + po.introspected = true + end + + # Generates, sets up and returns the proxy object. + def build + po = ProxyObject.new(@bus, @dest, @path) + ProxyObjectFactory.introspect_into(po, @xml) + po + end + end # class ProxyObjectFactory +end # module DBus + diff --git a/lib/dbus/marshall.rb b/lib/dbus/marshall.rb new file mode 100644 index 0000000..23a0f3b --- /dev/null +++ b/lib/dbus/marshall.rb @@ -0,0 +1,391 @@ +# dbus.rb - Module containing the low-level D-Bus implementation +# +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +require 'socket' + +# = D-Bus main module +# +# Module containing all the D-Bus modules and classes. +module DBus + # Exception raised when an invalid packet is encountered. + class InvalidPacketException < Exception + end + + # = D-Bus packet unmarshaller class + # + # Class that handles the conversion (unmarshalling) of payload data + # to Array. + class PacketUnmarshaller + # Index pointer that points to the byte in the data that is + # currently being processed. + # + # Used to kown what part of the buffer has been consumed by unmarshalling. + # FIXME: Maybe should be accessed with a "consumed_size" method. + attr_reader :idx + + # Create a new unmarshaller for the given data _buffer_ and _endianness_. + def initialize(buffer, endianness) + @buffy, @endianness = buffer.dup, endianness + if @endianness == BIG_END + @uint32 = "N" + @uint16 = "n" + @double = "G" + elsif @endianness == LIL_END + @uint32 = "V" + @uint16 = "v" + @double = "E" + else + # FIXME: shouldn't a more special exception be raised here? + # yes, idea for a good name ? :) + raise Exception, "Incorrect endianness" + end + @idx = 0 + end + + # Unmarshall the buffer for a given _signature_ and length _len_. + # Return an array of unmarshalled objects + def unmarshall(signature, len = nil) + if len != nil + if @buffy.size < @idx + len + raise IncompleteBufferException + end + end + sigtree = Type::Parser.new(signature).parse + ret = Array.new + sigtree.each do |elem| + ret << do_parse(elem) + end + ret + end + + # Align the pointer index on a byte index of _a_, where a + # must be 1, 2, 4 or 8. + def align(a) + case a + when 1 + when 2, 4, 8 + bits = a - 1 + @idx = @idx + bits & ~bits + raise IncompleteBufferException if @idx > @buffy.size + else + raise "Unsupported alignment #{a}" + end + end + + ############################################################### + # FIXME: does anyone except the object itself call the above methods? + # Yes : Message marshalling code needs to align "body" to 8 byte boundary + private + + # Retrieve the next _nbytes_ number of bytes from the buffer. + def get(nbytes) + raise IncompleteBufferException if @idx + nbytes > @buffy.size + ret = @buffy.slice(@idx, nbytes) + @idx += nbytes + ret + end + + # Retrieve the series of bytes until the next NULL (\0) byte. + def get_nul_terminated + raise IncompleteBufferException if not @buffy[@idx..-1] =~ /^([^\0]*)\0/ + str = $1 + raise IncompleteBufferException if @idx + str.size + 1 > @buffy.size + @idx += str.size + 1 + str + end + + # Get the string length and string itself from the buffer. + # Return the string. + def get_string + align(4) + str_sz = get(4).unpack(@uint32)[0] + ret = @buffy.slice(@idx, str_sz) + raise IncompleteBufferException if @idx + str_sz + 1 > @buffy.size + @idx += str_sz + if @buffy[@idx] != 0 + raise InvalidPacketException, "String is not nul-terminated" + end + @idx += 1 + # no exception, see check above + ret + end + + # Get the signature length and signature itself from the buffer. + # Return the signature. + def get_signature + str_sz = get(1).unpack('C')[0] + ret = @buffy.slice(@idx, str_sz) + raise IncompleteBufferException if @idx + str_sz + 1 >= @buffy.size + @idx += str_sz + if @buffy[@idx] != 0 + raise InvalidPacketException, "Type is not nul-terminated" + end + @idx += 1 + # no exception, see check above + ret + end + + # Based on the _signature_ type, retrieve a packet from the buffer + # and return it. + def do_parse(signature) + packet = nil + case signature.sigtype + when Type::BYTE + packet = get(1).unpack("C")[0] + when Type::UINT16 + align(2) + packet = get(2).unpack(@uint16)[0] + when Type::INT16 + align(4) + packet = get(4).unpack(@uint16)[0] + if (packet & 0x8000) != 0 + packet -= 0x10000 + end + when Type::UINT32 + align(4) + packet = get(4).unpack(@uint32)[0] + when Type::INT32 + align(4) + packet = get(4).unpack(@uint32)[0] + if (packet & 0x80000000) != 0 + packet -= 0x100000000 + end + when Type::UINT64 + align(8) + packet_l = get(4).unpack(@uint32)[0] + packet_h = get(4).unpack(@uint32)[0] + if @endianness == LIL_END + packet = packet_l + packet_h * 2**32 + else + packet = packet_l * 2**32 + packet_h + end + when Type::INT64 + align(8) + packet_l = get(4).unpack(@uint32)[0] + packet_h = get(4).unpack(@uint32)[0] + if @endianness == LIL_END + packet = packet_l + packet_h * 2**32 + else + packet = packet_l * 2**32 + packet_h + end + if (packet & 0x8000000000000000) != 0 + packet -= 0x10000000000000000 + end + when Type::DOUBLE + align(8) + packet = get(8).unpack(@double)[0] + when Type::BOOLEAN + align(4) + v = get(4).unpack(@uint32)[0] + raise InvalidPacketException if not [0, 1].member?(v) + packet = (v == 1) + when Type::ARRAY + align(4) + # checks please + array_sz = get(4).unpack(@uint32)[0] + raise InvalidPacketException if array_sz > 67108864 + + align(signature.child.alignment) + raise IncompleteBufferException if @idx + array_sz > @buffy.size + + packet = Array.new + start_idx = @idx + while @idx - start_idx < array_sz + packet << do_parse(signature.child) + end + + if signature.child.sigtype == Type::DICT_ENTRY then + packet = packet.inject(Hash.new) do |hash, pair| + hash[pair[0]] = pair[1] + hash + end + end + when Type::STRUCT + align(8) + packet = Array.new + signature.members.each do |elem| + packet << do_parse(elem) + end + when Type::VARIANT + string = get_signature + # error checking please + sig = Type::Parser.new(string).parse[0] + align(sig.alignment) + packet = do_parse(sig) + when Type::OBJECT_PATH + packet = get_string + when Type::STRING + packet = get_string + when Type::SIGNATURE + packet = get_signature + when Type::DICT_ENTRY + align(8) + key = do_parse(signature.members[0]) + value = do_parse(signature.members[1]) + packet = [key, value] + else + raise NotImplementedError, + "sigtype: #{signature.sigtype} (#{signature.sigtype.chr})" + end + packet + end # def do_parse + end # class PacketUnmarshaller + + # D-Bus packet marshaller class + # + # Class that handles the conversion (unmarshalling) of Ruby objects to + # (binary) payload data. + class PacketMarshaller + # The current or result packet. + # FIXME: allow access only when marshalling is finished + attr_reader :packet + + # Create a new marshaller, setting the current packet to the + # empty packet. + def initialize + @packet = "" + end + + # Align the buffer with NULL (\0) bytes on a byte length of _a_. + def align(a) + case a + when 1 + when 2, 4, 8 + bits = a - 1 + @packet = @packet.ljust(@packet.length + bits & ~bits, 0.chr) + else + raise "Unsupported alignment" + end + end + + # Append the the string _str_ itself to the packet. + def append_string(str) + align(4) + @packet += [str.length].pack("L") + str + "\0" + end + + # Append the the signature _signature_ itself to the packet. + def append_signature(str) + @packet += str.length.chr + str + "\0" + end + + # Append the array type _type_ to the packet and allow for appending + # the child elements. + def array(type) + # Thanks to Peter Rullmann for this line + align(4) + sizeidx = @packet.size + @packet += "ABCD" + align(type.alignment) + contentidx = @packet.size + yield + sz = @packet.size - contentidx + raise InvalidPacketException if sz > 67108864 + @packet[sizeidx...sizeidx + 4] = [sz].pack("L") + end + + # Align and allow for appending struct fields. + def struct + align(8) + yield + end + + # Append a string of bytes without type. + def append_simple_string(s) + @packet += s + "\0" + end + + # Append a value _val_ to the packet based on its _type_. + def append(type, val) + type = type.chr if type.kind_of?(Fixnum) + type = Type::Parser.new(type).parse[0] if type.kind_of?(String) + case type.sigtype + when Type::BYTE + @packet += val.chr + when Type::UINT32 + align(4) + @packet += [val].pack("L") + when Type::UINT64 + align(8) + @packet += [val].pack("Q") + when Type::INT64 + align(8) + @packet += [val].pack("q") + when Type::INT32 + align(4) + @packet += [val].pack("l") + when Type::UINT16 + align(2) + @packet += [val].pack("S") + when Type::INT16 + align(2) + @packet += [val].pack("s") + when Type::DOUBLE + align(8) + @packet += [val].pack("d") + when Type::BOOLEAN + align(4) + if val + @packet += [1].pack("L") + else + @packet += [0].pack("L") + end + when Type::OBJECT_PATH + append_string(val) + when Type::STRING + append_string(val) + when Type::SIGNATURE + append_signature(val) + when Type::VARIANT + if not val.kind_of?(Array) + raise TypeException + end + vartype, vardata = val + vartype = Type::Parser.new(vartype).parse[0] if vartype.kind_of?(String) + append_signature(vartype.to_s) + align(vartype.alignment) + sub = PacketMarshaller.new + sub.append(vartype, vardata) + @packet += sub.packet + when Type::ARRAY + if val.kind_of?(Hash) + raise TypeException if type.child.sigtype != Type::DICT_ENTRY + # Damn ruby rocks here + val = val.to_a + end + if not val.kind_of?(Array) + raise TypeException + end + array(type.child) do + val.each do |elem| + append(type.child, elem) + end + end + when Type::STRUCT, Type::DICT_ENTRY + raise TypeException if not val.kind_of?(Array) + if type.sigtype == Type::DICT_ENTRY and val.size != 2 + raise TypeException + end + struct do + idx = 0 + while val[idx] != nil + type.members.each do |subtype| + raise TypeException if val[idx] == nil + append(subtype, val[idx]) + idx += 1 + end + end + end + else + raise NotImplementedError + end + end # def append + end # class PacketMarshaller +end # module DBus diff --git a/lib/dbus/matchrule.rb b/lib/dbus/matchrule.rb new file mode 100644 index 0000000..b6f79f5 --- /dev/null +++ b/lib/dbus/matchrule.rb @@ -0,0 +1,98 @@ +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +module DBus + # Exception raised when an erroneous match rule type is encountered. + class MatchRuleException < Exception + end + + # = D-Bus match rule class + # + # FIXME + class MatchRule + # The list of possible match filters. + FILTERS = [:sender, :interface, :member, :path, :destination, :type] + # The sender filter. + attr_accessor :sender + # The interface filter. + attr_accessor :interface + # The member filter. + attr_accessor :member + # The path filter. + attr_accessor :path + # The destination filter. + attr_accessor :destination + # The type type that is matched. + attr_reader :type + + # Create a new match rule + def initialize + @sender = @interface = @member = @path = @destination = @type = nil + end + + # Set the message types to filter to type _t_. + # Possible message types are: signal, method_call, method_return, and error. + def type=(t) + if not ['signal', 'method_call', 'method_return', 'error'].member?(t) + raise MatchRuleException + end + @type = t + end + + # Returns a match rule string version of the object. + # E.g.: "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='Foo',path='/bar/foo',destination=':452345.34',arg2='bar'" + def to_s + FILTERS.select do |sym| + not method(sym).call.nil? + end.collect do |sym| + "#{sym.to_s}='#{method(sym).call}'" + end.join(",") + end + + # Parses a match rule string _s_ and sets the filters on the object. + def from_s(str) + s.split(",").each do |eq| + if eq =~ /^(.*)='([^']*)'$/ + name = $1 + val = $1 + if FILTERS.member?(name.to_sym) + method(name + "=").call(val) + else + raise MatchRuleException + end + end + end + end + + # Sets the match rule to filter for the given _signal_ and the + # given interface _intf_. + def from_signal(intf, signal) + signal = signal.name unless signal.is_a?(String) + self.type = "signal" + self.interface = intf.name + self.member = signal + self.path = intf.object.path + self + end + + # Determines whether a message _msg_ matches the match rule. + def match(msg) + if @type + if {Message::SIGNAL => "signal", Message::METHOD_CALL => "method_call", + Message::METHOD_RETURN => "method_return", + Message::ERROR => "error"}[msg.message_type] != @type + return false + end + end + return false if @interface and @interface != msg.interface + return false if @member and @member != msg.member + return false if @path and @path != msg.path + true + end + end # class MatchRule +end # module D-Bus diff --git a/lib/dbus/message.rb b/lib/dbus/message.rb new file mode 100644 index 0000000..a54796e --- /dev/null +++ b/lib/dbus/message.rb @@ -0,0 +1,280 @@ +# dbus.rb - Module containing the low-level D-Bus implementation +# +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +# = D-Bus main module +# +# Module containing all the D-Bus modules and classes. +module DBus + # = InvalidDestinationName class + # Thrown when you try do send a message to /org/freedesktop/DBus/Local, that + # is reserved. + class InvalidDestinationName < Exception + end + + # = D-Bus message class + # + # Class that holds any type of message that travels over the bus. + class Message + # The serial number of the message. + @@serial = 1 + # Mutex that protects updates on the serial number. + @@serial_mutex = Mutex.new + # Type of a message (by specification). + MESSAGE_SIGNATURE = "yyyyuua(yv)" + + # FIXME: following message type constants should be under Message::Type IMO + # well, yeah sure + # + # Invalid message type. + INVALID = 0 + # Method call message type. + METHOD_CALL = 1 + # Method call return value message type. + METHOD_RETURN = 2 + # Error message type. + ERROR = 3 + # Signal message type. + SIGNAL = 4 + + # Message flag signyfing that no reply is expected. + NO_REPLY_EXPECTED = 0x1 + # Message flag signifying that no automatic start is required/must be + # performed. + NO_AUTO_START = 0x2 + + # The type of the message. + attr_reader :message_type + # The path of the object instance the message must be sent to/is sent from. + attr_accessor :path + # The interface of the object that must be used/was used. + attr_accessor :interface + # The interface member (method/signal name) of the object that must be + # used/was used. + attr_accessor :member + # The name of the error (in case of an error message type). + attr_accessor :error_name + # The destination connection of the object that must be used/was used. + attr_accessor :destination + # The sender of the message. + attr_accessor :sender + # The signature of the message contents. + attr_accessor :signature + # The serial number of the message this message is a reply for. + attr_accessor :reply_serial + # The protocol. + attr_reader :protocol + # The serial of the message. + attr_reader :serial + # The parameters of the message. + attr_reader :params + + # Create a message with message type _mtype_ with default values and a + # unique serial number. + def initialize(mtype = INVALID) + @message_type = mtype + + @flags = 0 + @protocol = 1 + @body_length = 0 + @signature = String.new + @@serial_mutex.synchronize do + @serial = @@serial + @@serial += 1 + end + @params = Array.new + @destination = nil + @error_name = nil + @member = nil + @path = nil + @reply_serial = nil + + if mtype == METHOD_RETURN + @flags = NO_REPLY_EXPECTED + end + end + + # Mark this message as a reply to a another message _m_, taking + # the serial number of _m_ as reply serial and the sender of _m_ as + # destination. + def reply_to(m) + @message_type = METHOD_RETURN + @reply_serial = m.serial + @destination = m.sender + self + end + + # Add a parameter _val_ of type _type_ to the message. + def add_param(type, val) + type = type.chr if type.kind_of?(Fixnum) + @signature += type.to_s + @params << [type, val] + end + + # FIXME: what are these? a message element constant enumeration? + # See method below, in a message, you have and array of optional parameters + # that come with an index, to determine their meaning. The values are in + # spec, more a definition than an enumeration. + + PATH = 1 + INTERFACE = 2 + MEMBER = 3 + ERROR_NAME = 4 + REPLY_SERIAL = 5 + DESTINATION = 6 + SENDER = 7 + SIGNATURE = 8 + + # Marshall the message with its current set parameters and return + # it in a packet form. + def marshall + if @path == "/org/freedesktop/DBus/Local" + raise InvalidDestinationName + end + + params = PacketMarshaller.new + @params.each do |param| + params.append(param[0], param[1]) + end + @body_length = params.packet.length + + marshaller = PacketMarshaller.new + marshaller.append(Type::BYTE, HOST_END) + marshaller.append(Type::BYTE, @message_type) + marshaller.append(Type::BYTE, @flags) + marshaller.append(Type::BYTE, @protocol) + marshaller.append(Type::UINT32, @body_length) + marshaller.append(Type::UINT32, @serial) + marshaller.array(Type::Parser.new("y").parse[0]) do + if @path + marshaller.struct do + marshaller.append(Type::BYTE, PATH) + marshaller.append(Type::BYTE, 1) + marshaller.append_simple_string("o") + marshaller.append(Type::OBJECT_PATH, @path) + end + end + if @interface + marshaller.struct do + marshaller.append(Type::BYTE, INTERFACE) + marshaller.append(Type::BYTE, 1) + marshaller.append_simple_string("s") + marshaller.append(Type::STRING, @interface) + end + end + if @member + marshaller.struct do + marshaller.append(Type::BYTE, MEMBER) + marshaller.append(Type::BYTE, 1) + marshaller.append_simple_string("s") + marshaller.append(Type::STRING, @member) + end + end + if @error_name + marshaller.struct do + marshaller.append(Type::BYTE, ERROR_NAME) + marshaller.append(Type::BYTE, 1) + marshaller.append_simple_string("s") + marshaller.append(Type::STRING, @error_name) + end + end + if @reply_serial + marshaller.struct do + marshaller.append(Type::BYTE, REPLY_SERIAL) + marshaller.append(Type::BYTE, 1) + marshaller.append_simple_string("u") + marshaller.append(Type::UINT32, @reply_serial) + end + end + if @destination + marshaller.struct do + marshaller.append(Type::BYTE, DESTINATION) + marshaller.append(Type::BYTE, 1) + marshaller.append_simple_string("s") + marshaller.append(Type::STRING, @destination) + end + end + if @signature != "" + marshaller.struct do + marshaller.append(Type::BYTE, SIGNATURE) + marshaller.append(Type::BYTE, 1) + marshaller.append_simple_string("g") + marshaller.append(Type::SIGNATURE, @signature) + end + end + end + marshaller.align(8) + @params.each do |param| + marshaller.append(param[0], param[1]) + end + marshaller.packet + end + + # Unmarshall a packet contained in the buffer _buf_ and set the + # parameters of the message object according the data found in the + # buffer. + # Return the detected message and the index pointer of the buffer where + # the message data ended. + def unmarshall_buffer(buf) + buf = buf.dup + if buf[0] == ?l + endianness = LIL_END + else + endianness = BIG_END + end + pu = PacketUnmarshaller.new(buf, endianness) + mdata = pu.unmarshall(MESSAGE_SIGNATURE) + dummy, @message_type, @flags, @protocol, @body_length, @serial, + headers = mdata + + headers.each do |struct| + case struct[0] + when PATH + @path = struct[1] + when INTERFACE + @interface = struct[1] + when MEMBER + @member = struct[1] + when ERROR_NAME + @error_name = struct[1] + when REPLY_SERIAL + @reply_serial = struct[1] + when DESTINATION + @destination = struct[1] + when SENDER + @sender = struct[1] + when SIGNATURE + @signature = struct[1] + end + end + pu.align(8) + if @body_length > 0 and @signature + @params = pu.unmarshall(@signature, @body_length) + end + [self, pu.idx] + end # def unmarshall_buf + + # Unmarshall the data of a message found in the buffer _buf_ using + # Message#unmarshall_buf. + # Return the message. + def unmarshall(buf) + ret, size = unmarshall_buffer(buf) + ret + end + end # class Message + + # A helper exception on errors + class Error < Exception + attr_reader :dbus_message + def initialize(msg) + super(msg.error_name + ": " + msg.params.join(", ")) + @dbus_message = msg + end + end +end # module DBus diff --git a/lib/dbus/type.rb b/lib/dbus/type.rb new file mode 100644 index 0000000..83423f0 --- /dev/null +++ b/lib/dbus/type.rb @@ -0,0 +1,206 @@ +# dbus/type.rb - module containing low-level D-Bus data type information +# +# This file is part of the ruby-dbus project +# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License, version 2.1 as published by the Free Software Foundation. +# See the file "COPYING" for the exact licensing terms. + +module DBus + +# = D-Bus type module +# +# This module containts the constants of the types specified in the D-Bus +# protocol. +module Type + # The types. + INVALID = 0 + BYTE = ?y + BOOLEAN = ?b + INT16 = ?n + UINT16 = ?q + INT32 = ?i + UINT32 = ?u + INT64 = ?x + UINT64 = ?t + DOUBLE = ?d + STRUCT = ?r + ARRAY = ?a + VARIANT = ?v + OBJECT_PATH = ?o + STRING = ?s + SIGNATURE = ?g + DICT_ENTRY = ?e + + # Mapping from type number to name. + TypeName = { + INVALID => "INVALID", + BYTE => "BYTE", + BOOLEAN => "BOOLEAN", + INT16 => "INT16", + UINT16 => "UINT16", + INT32 => "INT32", + UINT32 => "UINT32", + INT64 => "INT64", + UINT64 => "UINT64", + DOUBLE => "DOUBLE", + STRUCT => "STRUCT", + ARRAY => "ARRAY", + VARIANT => "VARIANT", + OBJECT_PATH => "OBJECT_PATH", + STRING => "STRING", + SIGNATURE => "SIGNATURE", + DICT_ENTRY => "DICT_ENTRY" + } + + # Exception raised when an unknown/incorrect type is encountered. + class SignatureException < Exception + end + + # = D-Bus type conversion class + # + # Helper class for representing a D-Bus type. + class Type + # Returns the signature type number. + attr_reader :sigtype + # Return contained member types. + attr_reader :members + + # Create a new type instance for type number _sigtype_. + def initialize(sigtype) + if not TypeName.keys.member?(sigtype) + raise SignatureException, "Unknown key in signature: #{sigtype.chr}" + end + @sigtype = sigtype + @members = Array.new + end + + # Return the required alignment for the type. + def alignment + { + BYTE => 1, + BOOLEAN => 4, + INT16 => 2, + UINT16 => 2, + INT32 => 4, + UINT32 => 4, + INT64 => 8, + UINT64 => 8, + STRUCT => 8, + DICT_ENTRY => 8, + DOUBLE => 8, + ARRAY => 4, + OBJECT_PATH => 4, + STRING => 4, + SIGNATURE => 1, + }[@sigtype] + end + + # Return a string representation of the type according to the + # D-Bus specification. + def to_s + case @sigtype + when STRUCT + "(" + @members.collect { |t| t.to_s }.join + ")" + when ARRAY + "a" + @members.collect { |t| t.to_s } + when DICT_ENTRY + "{" + @members.collect { |t| t.to_s }.join + "}" + else + if not TypeName.keys.member?(@sigtype) + raise NotImplementedError + end + @sigtype.chr + end + end + + # Add a new member type _a_. + def <<(a) + if not [STRUCT, ARRAY, DICT_ENTRY].member?(@sigtype) + raise SignatureException + end + raise SignatureException if @sigtype == ARRAY and @members.size > 0 + if @sigtype == DICT_ENTRY + if @members.size == 2 + raise SignatureException, "Dict entries have exactly two members" + end + if @members.size == 0 + if [STRUCT, ARRAY, DICT_ENTRY].member?(a.sigtype) + raise SignatureException, "Dict entry keys must be basic types" + end + end + end + @members << a + end + + # Return the first contained member type. + def child + @members[0] + end + + def inspect + s = TypeName[@sigtype] + if [STRUCT, ARRAY].member?(@sigtype) + s += ": " + @members.inspect + end + s + end + end # class Type + + # = D-Bus type parser class + # + # Helper class to parse a type signature in the protocol. + class Parser + # Create a new parser for the given _signature_. + def initialize(signature) + @signature = signature + @idx = 0 + end + + # Returns the next character from the signature. + def nextchar + c = @signature[@idx] + @idx += 1 + c + end + + # Parse one character _c_ of the signature. + def parse_one(c) + res = nil + case c + when ?a + res = Type.new(ARRAY) + child = parse_one(nextchar) + res << child + when ?( + res = Type.new(STRUCT) + while (c = nextchar) != nil and c != ?) + res << parse_one(c) + end + raise SignatureException, "Parse error in #{@signature}" if c == nil + when ?{ + res = Type.new(DICT_ENTRY) + while (c = nextchar) != nil and c != ?} + res << parse_one(c) + end + raise SignatureException, "Parse error in #{@signature}" if c == nil + else + res = Type.new(c) + end + res + end + + # Parse the entire signature, return a DBus::Type object. + def parse + @idx = 0 + ret = Array.new + while (c = nextchar) + ret << parse_one(c) + end + ret + end + end # class Parser +end # module Type +end # module DBus diff --git a/setup.rb b/setup.rb new file mode 100644 index 0000000..424a5f3 --- /dev/null +++ b/setup.rb @@ -0,0 +1,1585 @@ +# +# setup.rb +# +# Copyright (c) 2000-2005 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +unless Errno.const_defined?(:ENOTEMPTY) # Windows? + module Errno + class ENOTEMPTY + # We do not raise this exception, implementation is not needed. + end + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted Windows' stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class ConfigTable + + include Enumerable + + def initialize(rbconfig) + @rbconfig = rbconfig + @items = [] + @table = {} + # options + @install_prefix = nil + @config_opt = nil + @verbose = true + @no_harm = false + end + + attr_accessor :install_prefix + attr_accessor :config_opt + + attr_writer :verbose + + def verbose? + @verbose + end + + attr_writer :no_harm + + def no_harm? + @no_harm + end + + def [](key) + lookup(key).resolve(self) + end + + def []=(key, val) + lookup(key).set val + end + + def names + @items.map {|i| i.name } + end + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or setup_rb_error "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def load_script(path, inst = nil) + if File.file?(path) + MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path + end + end + + def savefile + '.config' + end + + def load_savefile + begin + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + self[k] = v.strip + end + rescue Errno::ENOENT + setup_rb_error $!.message + "\n#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value? and i.value + end + } + end + + def load_standard_entries + standard_entries(@rbconfig).each do |ent| + add ent + end + end + + def standard_entries(rbconfig) + c = rbconfig + + rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + if c['rubylibdir'] + # V > 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = c['rubylibdir'] + librubyverarch = c['archdir'] + siteruby = c['sitedir'] + siterubyver = c['sitelibdir'] + siterubyverarch = c['sitearchdir'] + elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = c['sitedir'] + siterubyver = "$siteruby/#{version}" + siterubyverarch = "$siterubyver/#{c['arch']}" + else + # V < 1.4.4 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" + siterubyver = siteruby + siterubyverarch = "$siterubyver/#{c['arch']}" + end + parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') + } + + if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] + else + makeprog = 'make' + end + + [ + ExecItem.new('installdirs', 'std/site/home', + 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ + {|val, table| + case val + when 'std' + table['rbdir'] = '$librubyver' + table['sodir'] = '$librubyverarch' + when 'site' + table['rbdir'] = '$siterubyver' + table['sodir'] = '$siterubyverarch' + when 'home' + setup_rb_error '$HOME was not set' unless ENV['HOME'] + table['prefix'] = ENV['HOME'] + table['rbdir'] = '$libdir/ruby' + table['sodir'] = '$libdir/ruby' + end + }, + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', parameterize.call(c['libdir']), + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for system configuration files'), + PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), + 'the directory for local state data'), + PathItem.new('libruby', 'path', libruby, + 'the directory for ruby libraries'), + PathItem.new('librubyver', 'path', librubyver, + 'the directory for standard ruby libraries'), + PathItem.new('librubyverarch', 'path', librubyverarch, + 'the directory for standard ruby extensions'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') + ] + end + private :standard_entries + + def load_multipackage_entries + multipackage_entries().each do |ent| + add ent + end + end + + def multipackage_entries + [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') + ] + end + private :multipackage_entries + + ALIASES = { + 'std-ruby' => 'librubyver', + 'stdruby' => 'librubyver', + 'rubylibdir' => 'librubyver', + 'archdir' => 'librubyverarch', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } + + def fixup + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + @items.freeze + @table.freeze + @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ + end + + def parse_opt(opt) + m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" + m.to_a[1,2] + end + + def dllext + @rbconfig['DLEXT'] + end + + def value_config?(name) + lookup(name).value? + end + + class Item + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value? + true + end + + def value + @value + end + + def resolve(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end + end + + class BoolItem < Item + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + case val + when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' + when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' + else + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + end + end + + class PathItem < Item + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end + end + + class ProgramItem < Item + def config_type + 'program' + end + end + + class SelectItem < Item + def initialize(name, selection, default, desc) + super + @ok = selection.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end + end + + class ExecItem < Item + def initialize(name, selection, desc, &block) + super name, selection, nil, desc + @ok = selection.split('/') + @action = block + end + + def config_type + 'exec' + end + + def value? + false + end + + def resolve(table) + setup_rb_error "$#{name()} wrongly used as option value" + end + + undef set + + def evaluate(val, table) + v = val.strip.downcase + unless @ok.include?(v) + setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" + end + @action.call v, table + end + end + + class PackageSelectionItem < Item + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end + end + + class MetaConfigEnvironment + def initialize(config, installer) + @config = config + @installer = installer + end + + def config_names + @config.names + end + + def config?(name) + @config.key?(name) + end + + def bool_config?(name) + @config.lookup(name).config_type == 'bool' + end + + def path_config?(name) + @config.lookup(name).config_type == 'path' + end + + def value_config?(name) + @config.lookup(name).config_type != 'exec' + end + + def add_config(item) + @config.add item + end + + def add_bool_config(name, default, desc) + @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + @config.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + @config.lookup(name).default = default + end + + def remove_config(name) + @config.remove(name) + end + + # For only multipackage + def packages + raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer + @installer.packages + end + + # For only multipackage + def declare_packages(list) + raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer + @installer.packages = list + end + end + +end # class ConfigTable + + +# This module requires: #verbose?, #no_harm? +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # Does not check '/', it's too abnormal. + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(path) + $stderr.puts "rm -f #{path}" if verbose? + return if no_harm? + force_remove_file path + end + + def rm_rf(path) + $stderr.puts "rm -rf #{path}" if verbose? + return if no_harm? + remove_tree path + end + + def remove_tree(path) + if File.symlink?(path) + remove_file path + elsif File.dir?(path) + remove_tree0 path + else + force_remove_file path + end + end + + def remove_tree0(path) + Dir.foreach(path) do |ent| + next if ent == '.' + next if ent == '..' + entpath = "#{path}/#{ent}" + if File.symlink?(entpath) + remove_file entpath + elsif File.dir?(entpath) + remove_tree0 entpath + else + force_remove_file entpath + end + end + begin + Dir.rmdir path + rescue Errno::ENOTEMPTY + # directory may not be empty + end + end + + def move_file(src, dest) + force_remove_file dest + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| + f.write File.binread(src) + } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def force_remove_file(path) + begin + remove_file path + rescue + end + end + + def remove_file(path) + File.chmod 0777, path + File.unlink path + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(*args) + $stderr.puts args.join(' ') if verbose? + system(*args) or raise RuntimeError, + "system(#{args.map{|a| a.inspect }.join(' ')}) failed" + end + + def ruby(*args) + command config('rubyprog'), *args + end + + def make(task = nil) + command(*[config('makeprog'), task].compact) + end + + def extdir?(dir) + File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") + end + + def files_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.file?("#{dir}/#{ent}") } + } + end + + DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) + + def directories_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT + } + end + +end + + +# This module requires: #srcdir_root, #objdir_root, #relpath +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + # obsolete: use metaconfig to change configuration + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file?(srcfile(path)) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.4.1' + Copyright = 'Copyright (c) 2000-2005 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'test', 'run all tests in test/' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + config = ConfigTable.new(load_rbconfig()) + config.load_standard_entries + config.load_multipackage_entries if multipackage? + config.fixup + klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) + klass.new(File.dirname($0), config).invoke + end + + def ToplevelInstaller.multipackage? + File.dir?(File.dirname($0) + '/packages') + end + + def ToplevelInstaller.load_rbconfig + if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + load File.expand_path(arg.split(/=/, 2)[1]) + $".push 'rbconfig.rb' + else + require 'rbconfig' + end + ::Config::CONFIG + end + + def initialize(ardir_root, config) + @ardir = File.expand_path(ardir_root) + @config = config + # cache + @valid_task_re = nil + end + + def config(key) + @config[key] + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + case task + when 'config', 'test' + ; + when 'clean', 'distclean' + @config.load_savefile if File.exist?(@config.savefile) + else + @config.load_savefile + end + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig" + end + + def init_installers + @installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) + return arg + when '-q', '--quiet' + @config.verbose = false + when '--verbose' + @config.verbose = true + when '--help' + print_usage $stdout + exit 0 + when '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + when '--copyright' + puts Copyright + exit 0 + else + setup_rb_error "unknown global option '#{arg}'" + end + end + nil + end + + def valid_task?(t) + valid_task_re() =~ t + end + + def valid_task_re + @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ + end + + def parsearg_no_options + unless ARGV.empty? + task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) + setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_test parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + evalopt = [] + set = [] + @config.config_opt = [] + while i = ARGV.shift + if /\A--?\z/ =~ i + @config.config_opt = ARGV.dup + break + end + name, value = *@config.parse_opt(i) + if @config.value_config?(name) + @config[name] = value + else + evalopt.push [name, value] + end + set.push name + end + evalopt.each do |name, value| + @config.lookup(name).evaluate value, @config + end + # Check if configuration is valid + set.each do |n| + @config[n] if @config.value_config?(n) + end + end + + def parsearg_install + @config.no_harm = false + @config.install_prefix = '' + while a = ARGV.shift + case a + when '--no-harm' + @config.no_harm = true + when /\A--prefix=/ + path = a.split(/=/, 2)[1] + path = File.expand_path(path) unless path[0,1] == '/' + @config.install_prefix = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, ' --help', 'print this message' + out.printf fmt, ' --version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + @config.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_test + @installer.exec_test + end + + def exec_show + @config.each do |i| + printf "%-20s %s\n", i.name, i.value if i.value? + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end # class ToplevelInstaller + + +class ToplevelInstallerMulti < ToplevelInstaller + + include FileOperations + + def initialize(ardir_root, config) + super + @packages = directories_of("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig", self + @packages.each do |name| + @config.load_script "#{@ardir}/packages/#{name}/metaconfig" + end + end + + attr_reader :packages + + def packages=(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_test + run_hook 'pre-test' + each_selected_installers {|inst| inst.exec_test } + run_hook 'post-test' + end + + def exec_clean + rm_f @config.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f @config.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if verbose? + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def run_hook(id) + @root_installer.run_hook id + end + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + +end # class ToplevelInstallerMulti + + +class Installer + + FILETYPES = %w( bin lib ext data conf man ) + + include FileOperations + include HookScriptAPI + + def initialize(config, srcroot, objroot) + @config = config + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + def noop(rel) + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # Config Access + # + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + + def verbose_off + begin + save, @config.verbose = @config.verbose?, false + yield + ensure + @config.verbose = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + alias config_dir_bin noop + alias config_dir_lib noop + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + alias config_dir_data noop + alias config_dir_conf noop + alias config_dir_man noop + + def extconf + ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + files_of(curr_srcdir()).each do |fname| + update_shebang_line "#{curr_srcdir()}/#{fname}" + end + end + + alias setup_dir_lib noop + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + alias setup_dir_data noop + alias setup_dir_conf noop + alias setup_dir_man noop + + def update_shebang_line(path) + return if no_harm? + return if config('shebang') == 'never' + old = Shebang.load(path) + if old + $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 + new = new_shebang(old) + return if new.to_s == old.to_s + else + return unless config('shebang') == 'all' + new = Shebang.new(config('rubypath')) + end + $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? + open_atomic_writer(path) {|output| + File.open(path, 'rb') {|f| + f.gets if old # discard + output.puts new.to_s + output.print f.read + } + } + end + + def new_shebang(old) + if /\Aruby/ =~ File.basename(old.cmd) + Shebang.new(config('rubypath'), old.args) + elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' + Shebang.new(config('rubypath'), old.args[1..-1]) + else + return old unless config('shebang') == 'all' + Shebang.new(config('rubypath')) + end + end + + def open_atomic_writer(path, &block) + tmpfile = File.basename(path) + '.tmp' + begin + File.open(tmpfile, 'wb', &block) + File.rename tmpfile, File.basename(path) + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + class Shebang + def Shebang.load(path) + line = nil + File.open(path) {|f| + line = f.gets + } + return nil unless /\A#!/ =~ line + parse(line) + end + + def Shebang.parse(line) + cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') + new(cmd, args) + end + + def initialize(cmd, args = []) + @cmd = cmd + @args = args + end + + attr_reader :cmd + attr_reader :args + + def to_s + "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") + end + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files rubyextentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_dir_conf(rel) + # FIXME: should not remove current config files + # (rename previous file to .old/.org) + install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 + end + + def install_dir_man(rel) + install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @config.install_prefix + list.each do |fname| + install fname, dest, mode, @config.install_prefix + end + end + + def libfiles + glob_reject(%w(*.y *.output), targetfiles()) + end + + def rubyextentions(dir) + ents = glob_select("*.#{@config.dllext}", targetfiles()) + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + ents + end + + def targetfiles + mapdir(existfiles() - hookfiles()) + end + + def mapdir(ents) + ents.map {|ent| + if File.exist?(ent) + then ent # objdir + else "#{curr_srcdir()}/#{ent}" # srcdir + end + } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + JUNK_FILES = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + + def existfiles + glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def glob_select(pat, ents) + re = globs2re([pat]) + ents.select {|ent| re =~ ent } + end + + def glob_reject(pats, ents) + re = globs2re(pats) + ents.reject {|ent| re =~ ent } + end + + GLOB2REGEX = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + + def globs2re(pats) + /\A(?:#{ + pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') + })\z/ + end + + # + # TASK test + # + + TESTDIR = 'test' + + def exec_test + unless File.directory?('test') + $stderr.puts 'no test in this package' if verbose? + return + end + $stderr.puts 'Running tests...' if verbose? + begin + require 'test/unit' + rescue LoadError + setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' + end + runner = Test::Unit::AutoRunner.new(true) + runner.to_run << TESTDIR + runner.run + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias clean_dir_bin noop + alias clean_dir_lib noop + alias clean_dir_data noop + alias clean_dir_conf noop + alias clean_dir_man noop + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias distclean_dir_bin noop + alias distclean_dir_lib noop + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + alias distclean_dir_data noop + alias distclean_dir_conf noop + alias distclean_dir_man noop + + # + # Traversing + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if type == 'ext' and config('without-ext') == 'yes' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + directories_of(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + def run_hook(id) + path = [ "#{curr_srcdir()}/#{id}", + "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } + return unless path + begin + instance_eval File.read(path), path, 1 + rescue + raise if $DEBUG + setup_rb_error "hook #{path} failed:\n" + $!.message + end + end + +end # class Installer + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +if $0 == __FILE__ + begin + ToplevelInstaller.invoke + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end