├── .dir-locals.el ├── .gitignore ├── COPYING.LIB ├── ChangeLog ├── Makefile.in ├── PKG-INFO.src ├── README ├── TODO ├── configure.in ├── howto-make-a-new-version ├── install-sh ├── mkinstalldirs ├── py_interface ├── __init__.py.src ├── erl_async_conn.py ├── erl_common.py ├── erl_epmd.py ├── erl_eventhandler.py ├── erl_node.py ├── erl_node_conn.py ├── erl_opts.py └── erl_term.py ├── setup.py.src ├── test ├── Makefile.in ├── out_connecting.py ├── pingpong_master_tests.erl ├── pingpong_slave.py ├── py_node_mgr.erl ├── qc_pingpong_master_tests.erl ├── rpc_caller.py └── test_erl_term.py └── vsn /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ( 2 | ;; For all modes (types of files) 3 | (nil . ( 4 | ;; use spaces to indent, not tabs 5 | (indent-tabs-mode . nil) 6 | ;; but if there are any tabs, the tab with is 8 chars anyway 7 | (tab-width . 8) 8 | ;; Lines no longer than this 9 | (fill-column . 80))) 10 | 11 | ;; However, for Makefiles, tabs are important 12 | (makefile-mode . ((indent-tabs-mode . t))) 13 | 14 | ;; For Erlang files 15 | (erlang-mode . ( 16 | ;; Use this spaces of indentation per level 17 | (erlang-indent-level . 4))) 18 | ;; For Python files 19 | (python-mode . ( 20 | ;; Use this many spaces of indentation per level 21 | (py-indent-offset . 4)))) 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test_erl_node_pingpong.log-py 2 | *.beam 3 | *.pyc 4 | autom4te.cache 5 | config.cache 6 | config.status 7 | configure 8 | Makefile 9 | !examples/Makefile 10 | __init__.py 11 | *~ 12 | setup.py 13 | PKG-INFO 14 | config.log 15 | py_interface-*.tar.gz 16 | py_interface-*.tgz 17 | examples/test_erl_node_pingpong.log-py 18 | examples/test_erl_node_pingpong_qc.log-py 19 | /build 20 | /test/test_erl_node_pingpong.log-py 21 | /test/test_erl_node_pingpong_qc.log-py 22 | current_counterexample.* 23 | erl_crash.dump 24 | -------------------------------------------------------------------------------- /COPYING.LIB: -------------------------------------------------------------------------------- 1 | GNU LIBRARY GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the library GPL. It is 10 | numbered 2 because it goes with version 2 of the ordinary GPL.] 11 | 12 | Preamble 13 | 14 | The licenses for most software are designed to take away your 15 | freedom to share and change it. By contrast, the GNU General Public 16 | Licenses are intended to guarantee your freedom to share and change 17 | free software--to make sure the software is free for all its users. 18 | 19 | This license, the Library General Public License, applies to some 20 | specially designated Free Software Foundation software, and to any 21 | other libraries whose authors decide to use it. You can use it for 22 | your libraries, too. 23 | 24 | When we speak of free software, we are referring to freedom, not 25 | price. Our General Public Licenses are designed to make sure that you 26 | have the freedom to distribute copies of free software (and charge for 27 | this service if you wish), that you receive source code or can get it 28 | if you want it, that you can change the software or use pieces of it 29 | in new free programs; and that you know you can do these things. 30 | 31 | To protect your rights, we need to make restrictions that forbid 32 | anyone to deny you these rights or to ask you to surrender the rights. 33 | These restrictions translate to certain responsibilities for you if 34 | you distribute copies of the library, or if you modify it. 35 | 36 | For example, if you distribute copies of the library, whether gratis 37 | or for a fee, you must give the recipients all the rights that we gave 38 | you. You must make sure that they, too, receive or can get the source 39 | code. If you link a program with the library, you must provide 40 | complete object files to the recipients so that they can relink them 41 | with the library, after making changes to the library and recompiling 42 | it. And you must show them these terms so they know their rights. 43 | 44 | Our method of protecting your rights has two steps: (1) copyright 45 | the library, and (2) offer you this license which gives you legal 46 | permission to copy, distribute and/or modify the library. 47 | 48 | Also, for each distributor's protection, we want to make certain 49 | that everyone understands that there is no warranty for this free 50 | library. If the library is modified by someone else and passed on, we 51 | want its recipients to know that what they have is not the original 52 | version, so that any problems introduced by others will not reflect on 53 | the original authors' reputations. 54 | 55 | Finally, any free program is threatened constantly by software 56 | patents. We wish to avoid the danger that companies distributing free 57 | software will individually obtain patent licenses, thus in effect 58 | transforming the program into proprietary software. To prevent this, 59 | we have made it clear that any patent must be licensed for everyone's 60 | free use or not licensed at all. 61 | 62 | Most GNU software, including some libraries, is covered by the ordinary 63 | GNU General Public License, which was designed for utility programs. This 64 | license, the GNU Library General Public License, applies to certain 65 | designated libraries. This license is quite different from the ordinary 66 | one; be sure to read it in full, and don't assume that anything in it is 67 | the same as in the ordinary license. 68 | 69 | The reason we have a separate public license for some libraries is that 70 | they blur the distinction we usually make between modifying or adding to a 71 | program and simply using it. Linking a program with a library, without 72 | changing the library, is in some sense simply using the library, and is 73 | analogous to running a utility program or application program. However, in 74 | a textual and legal sense, the linked executable is a combined work, a 75 | derivative of the original library, and the ordinary General Public License 76 | treats it as such. 77 | 78 | Because of this blurred distinction, using the ordinary General 79 | Public License for libraries did not effectively promote software 80 | sharing, because most developers did not use the libraries. We 81 | concluded that weaker conditions might promote sharing better. 82 | 83 | However, unrestricted linking of non-free programs would deprive the 84 | users of those programs of all benefit from the free status of the 85 | libraries themselves. This Library General Public License is intended to 86 | permit developers of non-free programs to use free libraries, while 87 | preserving your freedom as a user of such programs to change the free 88 | libraries that are incorporated in them. (We have not seen how to achieve 89 | this as regards changes in header files, but we have achieved it as regards 90 | changes in the actual functions of the Library.) The hope is that this 91 | will lead to faster development of free libraries. 92 | 93 | The precise terms and conditions for copying, distribution and 94 | modification follow. Pay close attention to the difference between a 95 | "work based on the library" and a "work that uses the library". The 96 | former contains code derived from the library, while the latter only 97 | works together with the library. 98 | 99 | Note that it is possible for a library to be covered by the ordinary 100 | General Public License rather than by this special one. 101 | 102 | GNU LIBRARY GENERAL PUBLIC LICENSE 103 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 104 | 105 | 0. This License Agreement applies to any software library which 106 | contains a notice placed by the copyright holder or other authorized 107 | party saying it may be distributed under the terms of this Library 108 | General Public License (also called "this License"). Each licensee is 109 | addressed as "you". 110 | 111 | A "library" means a collection of software functions and/or data 112 | prepared so as to be conveniently linked with application programs 113 | (which use some of those functions and data) to form executables. 114 | 115 | The "Library", below, refers to any such software library or work 116 | which has been distributed under these terms. A "work based on the 117 | Library" means either the Library or any derivative work under 118 | copyright law: that is to say, a work containing the Library or a 119 | portion of it, either verbatim or with modifications and/or translated 120 | straightforwardly into another language. (Hereinafter, translation is 121 | included without limitation in the term "modification".) 122 | 123 | "Source code" for a work means the preferred form of the work for 124 | making modifications to it. For a library, complete source code means 125 | all the source code for all modules it contains, plus any associated 126 | interface definition files, plus the scripts used to control compilation 127 | and installation of the library. 128 | 129 | Activities other than copying, distribution and modification are not 130 | covered by this License; they are outside its scope. The act of 131 | running a program using the Library is not restricted, and output from 132 | such a program is covered only if its contents constitute a work based 133 | on the Library (independent of the use of the Library in a tool for 134 | writing it). Whether that is true depends on what the Library does 135 | and what the program that uses the Library does. 136 | 137 | 1. You may copy and distribute verbatim copies of the Library's 138 | complete source code as you receive it, in any medium, provided that 139 | you conspicuously and appropriately publish on each copy an 140 | appropriate copyright notice and disclaimer of warranty; keep intact 141 | all the notices that refer to this License and to the absence of any 142 | warranty; and distribute a copy of this License along with the 143 | Library. 144 | 145 | You may charge a fee for the physical act of transferring a copy, 146 | and you may at your option offer warranty protection in exchange for a 147 | fee. 148 | 149 | 2. You may modify your copy or copies of the Library or any portion 150 | of it, thus forming a work based on the Library, and copy and 151 | distribute such modifications or work under the terms of Section 1 152 | above, provided that you also meet all of these conditions: 153 | 154 | a) The modified work must itself be a software library. 155 | 156 | b) You must cause the files modified to carry prominent notices 157 | stating that you changed the files and the date of any change. 158 | 159 | c) You must cause the whole of the work to be licensed at no 160 | charge to all third parties under the terms of this License. 161 | 162 | d) If a facility in the modified Library refers to a function or a 163 | table of data to be supplied by an application program that uses 164 | the facility, other than as an argument passed when the facility 165 | is invoked, then you must make a good faith effort to ensure that, 166 | in the event an application does not supply such function or 167 | table, the facility still operates, and performs whatever part of 168 | its purpose remains meaningful. 169 | 170 | (For example, a function in a library to compute square roots has 171 | a purpose that is entirely well-defined independent of the 172 | application. Therefore, Subsection 2d requires that any 173 | application-supplied function or table used by this function must 174 | be optional: if the application does not supply it, the square 175 | root function must still compute square roots.) 176 | 177 | These requirements apply to the modified work as a whole. If 178 | identifiable sections of that work are not derived from the Library, 179 | and can be reasonably considered independent and separate works in 180 | themselves, then this License, and its terms, do not apply to those 181 | sections when you distribute them as separate works. But when you 182 | distribute the same sections as part of a whole which is a work based 183 | on the Library, the distribution of the whole must be on the terms of 184 | this License, whose permissions for other licensees extend to the 185 | entire whole, and thus to each and every part regardless of who wrote 186 | it. 187 | 188 | Thus, it is not the intent of this section to claim rights or contest 189 | your rights to work written entirely by you; rather, the intent is to 190 | exercise the right to control the distribution of derivative or 191 | collective works based on the Library. 192 | 193 | In addition, mere aggregation of another work not based on the Library 194 | with the Library (or with a work based on the Library) on a volume of 195 | a storage or distribution medium does not bring the other work under 196 | the scope of this License. 197 | 198 | 3. You may opt to apply the terms of the ordinary GNU General Public 199 | License instead of this License to a given copy of the Library. To do 200 | this, you must alter all the notices that refer to this License, so 201 | that they refer to the ordinary GNU General Public License, version 2, 202 | instead of to this License. (If a newer version than version 2 of the 203 | ordinary GNU General Public License has appeared, then you can specify 204 | that version instead if you wish.) Do not make any other change in 205 | these notices. 206 | 207 | Once this change is made in a given copy, it is irreversible for 208 | that copy, so the ordinary GNU General Public License applies to all 209 | subsequent copies and derivative works made from that copy. 210 | 211 | This option is useful when you wish to copy part of the code of 212 | the Library into a program that is not a library. 213 | 214 | 4. You may copy and distribute the Library (or a portion or 215 | derivative of it, under Section 2) in object code or executable form 216 | under the terms of Sections 1 and 2 above provided that you accompany 217 | it with the complete corresponding machine-readable source code, which 218 | must be distributed under the terms of Sections 1 and 2 above on a 219 | medium customarily used for software interchange. 220 | 221 | If distribution of object code is made by offering access to copy 222 | from a designated place, then offering equivalent access to copy the 223 | source code from the same place satisfies the requirement to 224 | distribute the source code, even though third parties are not 225 | compelled to copy the source along with the object code. 226 | 227 | 5. A program that contains no derivative of any portion of the 228 | Library, but is designed to work with the Library by being compiled or 229 | linked with it, is called a "work that uses the Library". Such a 230 | work, in isolation, is not a derivative work of the Library, and 231 | therefore falls outside the scope of this License. 232 | 233 | However, linking a "work that uses the Library" with the Library 234 | creates an executable that is a derivative of the Library (because it 235 | contains portions of the Library), rather than a "work that uses the 236 | library". The executable is therefore covered by this License. 237 | Section 6 states terms for distribution of such executables. 238 | 239 | When a "work that uses the Library" uses material from a header file 240 | that is part of the Library, the object code for the work may be a 241 | derivative work of the Library even though the source code is not. 242 | Whether this is true is especially significant if the work can be 243 | linked without the Library, or if the work is itself a library. The 244 | threshold for this to be true is not precisely defined by law. 245 | 246 | If such an object file uses only numerical parameters, data 247 | structure layouts and accessors, and small macros and small inline 248 | functions (ten lines or less in length), then the use of the object 249 | file is unrestricted, regardless of whether it is legally a derivative 250 | work. (Executables containing this object code plus portions of the 251 | Library will still fall under Section 6.) 252 | 253 | Otherwise, if the work is a derivative of the Library, you may 254 | distribute the object code for the work under the terms of Section 6. 255 | Any executables containing that work also fall under Section 6, 256 | whether or not they are linked directly with the Library itself. 257 | 258 | 6. As an exception to the Sections above, you may also compile or 259 | link a "work that uses the Library" with the Library to produce a 260 | work containing portions of the Library, and distribute that work 261 | under terms of your choice, provided that the terms permit 262 | modification of the work for the customer's own use and reverse 263 | engineering for debugging such modifications. 264 | 265 | You must give prominent notice with each copy of the work that the 266 | Library is used in it and that the Library and its use are covered by 267 | this License. You must supply a copy of this License. If the work 268 | during execution displays copyright notices, you must include the 269 | copyright notice for the Library among them, as well as a reference 270 | directing the user to the copy of this License. Also, you must do one 271 | of these things: 272 | 273 | a) Accompany the work with the complete corresponding 274 | machine-readable source code for the Library including whatever 275 | changes were used in the work (which must be distributed under 276 | Sections 1 and 2 above); and, if the work is an executable linked 277 | with the Library, with the complete machine-readable "work that 278 | uses the Library", as object code and/or source code, so that the 279 | user can modify the Library and then relink to produce a modified 280 | executable containing the modified Library. (It is understood 281 | that the user who changes the contents of definitions files in the 282 | Library will not necessarily be able to recompile the application 283 | to use the modified definitions.) 284 | 285 | b) Accompany the work with a written offer, valid for at 286 | least three years, to give the same user the materials 287 | specified in Subsection 6a, above, for a charge no more 288 | than the cost of performing this distribution. 289 | 290 | c) If distribution of the work is made by offering access to copy 291 | from a designated place, offer equivalent access to copy the above 292 | specified materials from the same place. 293 | 294 | d) Verify that the user has already received a copy of these 295 | materials or that you have already sent this user a copy. 296 | 297 | For an executable, the required form of the "work that uses the 298 | Library" must include any data and utility programs needed for 299 | reproducing the executable from it. However, as a special exception, 300 | the source code distributed need not include anything that is normally 301 | distributed (in either source or binary form) with the major 302 | components (compiler, kernel, and so on) of the operating system on 303 | which the executable runs, unless that component itself accompanies 304 | the executable. 305 | 306 | It may happen that this requirement contradicts the license 307 | restrictions of other proprietary libraries that do not normally 308 | accompany the operating system. Such a contradiction means you cannot 309 | use both them and the Library together in an executable that you 310 | distribute. 311 | 312 | 7. You may place library facilities that are a work based on the 313 | Library side-by-side in a single library together with other library 314 | facilities not covered by this License, and distribute such a combined 315 | library, provided that the separate distribution of the work based on 316 | the Library and of the other library facilities is otherwise 317 | permitted, and provided that you do these two things: 318 | 319 | a) Accompany the combined library with a copy of the same work 320 | based on the Library, uncombined with any other library 321 | facilities. This must be distributed under the terms of the 322 | Sections above. 323 | 324 | b) Give prominent notice with the combined library of the fact 325 | that part of it is a work based on the Library, and explaining 326 | where to find the accompanying uncombined form of the same work. 327 | 328 | 8. You may not copy, modify, sublicense, link with, or distribute 329 | the Library except as expressly provided under this License. Any 330 | attempt otherwise to copy, modify, sublicense, link with, or 331 | distribute the Library is void, and will automatically terminate your 332 | rights under this License. However, parties who have received copies, 333 | or rights, from you under this License will not have their licenses 334 | terminated so long as such parties remain in full compliance. 335 | 336 | 9. You are not required to accept this License, since you have not 337 | signed it. However, nothing else grants you permission to modify or 338 | distribute the Library or its derivative works. These actions are 339 | prohibited by law if you do not accept this License. Therefore, by 340 | modifying or distributing the Library (or any work based on the 341 | Library), you indicate your acceptance of this License to do so, and 342 | all its terms and conditions for copying, distributing or modifying 343 | the Library or works based on it. 344 | 345 | 10. Each time you redistribute the Library (or any work based on the 346 | Library), the recipient automatically receives a license from the 347 | original licensor to copy, distribute, link with or modify the Library 348 | subject to these terms and conditions. You may not impose any further 349 | restrictions on the recipients' exercise of the rights granted herein. 350 | You are not responsible for enforcing compliance by third parties to 351 | this License. 352 | 353 | 11. If, as a consequence of a court judgment or allegation of patent 354 | infringement or for any other reason (not limited to patent issues), 355 | conditions are imposed on you (whether by court order, agreement or 356 | otherwise) that contradict the conditions of this License, they do not 357 | excuse you from the conditions of this License. If you cannot 358 | distribute so as to satisfy simultaneously your obligations under this 359 | License and any other pertinent obligations, then as a consequence you 360 | may not distribute the Library at all. For example, if a patent 361 | license would not permit royalty-free redistribution of the Library by 362 | all those who receive copies directly or indirectly through you, then 363 | the only way you could satisfy both it and this License would be to 364 | refrain entirely from distribution of the Library. 365 | 366 | If any portion of this section is held invalid or unenforceable under any 367 | particular circumstance, the balance of the section is intended to apply, 368 | and the section as a whole is intended to apply in other circumstances. 369 | 370 | It is not the purpose of this section to induce you to infringe any 371 | patents or other property right claims or to contest validity of any 372 | such claims; this section has the sole purpose of protecting the 373 | integrity of the free software distribution system which is 374 | implemented by public license practices. Many people have made 375 | generous contributions to the wide range of software distributed 376 | through that system in reliance on consistent application of that 377 | system; it is up to the author/donor to decide if he or she is willing 378 | to distribute software through any other system and a licensee cannot 379 | impose that choice. 380 | 381 | This section is intended to make thoroughly clear what is believed to 382 | be a consequence of the rest of this License. 383 | 384 | 12. If the distribution and/or use of the Library is restricted in 385 | certain countries either by patents or by copyrighted interfaces, the 386 | original copyright holder who places the Library under this License may add 387 | an explicit geographical distribution limitation excluding those countries, 388 | so that distribution is permitted only in or among countries not thus 389 | excluded. In such case, this License incorporates the limitation as if 390 | written in the body of this License. 391 | 392 | 13. The Free Software Foundation may publish revised and/or new 393 | versions of the Library General Public License from time to time. 394 | Such new versions will be similar in spirit to the present version, 395 | but may differ in detail to address new problems or concerns. 396 | 397 | Each version is given a distinguishing version number. If the Library 398 | specifies a version number of this License which applies to it and 399 | "any later version", you have the option of following the terms and 400 | conditions either of that version or of any later version published by 401 | the Free Software Foundation. If the Library does not specify a 402 | license version number, you may choose any version ever published by 403 | the Free Software Foundation. 404 | 405 | 14. If you wish to incorporate parts of the Library into other free 406 | programs whose distribution conditions are incompatible with these, 407 | write to the author to ask for permission. For software which is 408 | copyrighted by the Free Software Foundation, write to the Free 409 | Software Foundation; we sometimes make exceptions for this. Our 410 | decision will be guided by the two goals of preserving the free status 411 | of all derivatives of our free software and of promoting the sharing 412 | and reuse of software generally. 413 | 414 | NO WARRANTY 415 | 416 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 417 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 418 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 419 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 420 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 421 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 422 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 423 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 424 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 425 | 426 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 427 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 428 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 429 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 430 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 431 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 432 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 433 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 434 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 435 | DAMAGES. 436 | 437 | END OF TERMS AND CONDITIONS 438 | 439 | How to Apply These Terms to Your New Libraries 440 | 441 | If you develop a new library, and you want it to be of the greatest 442 | possible use to the public, we recommend making it free software that 443 | everyone can redistribute and change. You can do so by permitting 444 | redistribution under these terms (or, alternatively, under the terms of the 445 | ordinary General Public License). 446 | 447 | To apply these terms, attach the following notices to the library. It is 448 | safest to attach them to the start of each source file to most effectively 449 | convey the exclusion of warranty; and each file should have at least the 450 | "copyright" line and a pointer to where the full notice is found. 451 | 452 | 453 | Copyright (C) 454 | 455 | This library is free software; you can redistribute it and/or 456 | modify it under the terms of the GNU Library General Public 457 | License as published by the Free Software Foundation; either 458 | version 2 of the License, or (at your option) any later version. 459 | 460 | This library is distributed in the hope that it will be useful, 461 | but WITHOUT ANY WARRANTY; without even the implied warranty of 462 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 463 | Library General Public License for more details. 464 | 465 | You should have received a copy of the GNU Library General Public 466 | License along with this library; if not, write to the Free 467 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 468 | 469 | Also add information on how to contact you by electronic and paper mail. 470 | 471 | You should also get your employer (if you work as a programmer) or your 472 | school, if any, to sign a "copyright disclaimer" for the library, if 473 | necessary. Here is a sample; alter the names: 474 | 475 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 476 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 477 | 478 | , 1 April 1990 479 | Ty Coon, President of Vice 480 | 481 | That's all there is to it! 482 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | version 2.3, 2018-Jan-03 2 | 3 | * allow to specify Epmd host and port number through ErlNodeOpts 4 | Thanks to Dmitry Shestak 5 | 6 | version 2.2, 2017-Jul-11 7 | 8 | * Adapt to Erlang 20: Encode all atoms as UTF8, set the UTF8ATOMS 9 | distribution flag (Erlang 20 won't accept a node witout this). Retain 10 | decoding capability for old-format atoms, but encode to UTF-8 atoms, 11 | which has been supported by Erlang since R16. 12 | 13 | This also means No longer any support for Erlang R15 or ealier. (The 14 | last Erlang R15 version was released in the end of the year 2012.) 15 | 16 | version 2.1, 2017-Jan-11 17 | 18 | * Bugfix unpacking of strings: they were accidentally unpacked to 19 | bytes, now they are unpacked to strings. Also added two 20 | utillity functions: erl_term.IODataToStr and erl_term.StrToList. 21 | * Change type represenatation of binaries, from the ErlBinary 22 | class, to bytes, since this ought to be a more natural fit for 23 | Python 3. 24 | * Improve README 25 | 26 | version 2.0, 2017-Jan-07 27 | 28 | * Python 3 support (only). Dropped support for Python 2. 29 | (The changes to the packing and unpacking code, mostly, 30 | were too pervasive, to support both Python 2 an 3 31 | in the same code base.) 32 | 33 | version 1.4, 2017-Jan-07 34 | 35 | NB: This is the last version with support for Python 2 36 | From 2.0, it is Python 3. Bugfixes might go in 1.4.x, if important enough, 37 | but any new development is intended for Python 3 only. 38 | 39 | * Bugfix packing of improper lists on the Python side 40 | * In the ErlMap type, which is a dict, make ints and floats, 41 | eg 0 and 0.0, be different keys. This differs from normal Python 42 | dicts. 43 | * Rework the homebrewn tests into Erlang eunit tests. 44 | * Add some README documentation and drop the old examples dir 45 | 46 | version 1.3, 2014-Apr-25 47 | 48 | * Add support for encoding of new floats (8 bytes) 49 | * Add support for encoding of bit strings 50 | * Add support for exports (fun M:F/A) 51 | * Add support for funs (fun(...) -> ... end, and fun X/A) 52 | * Encode/decode depending on the peer's advertised distr flags 53 | * Convert exceptions from old-style deprecated strings, to classes 54 | 55 | version 1.2.1, 2014-Apr-21 56 | 57 | * Fix a bug in the dist top-level make target 58 | 59 | version 1.2, 2014-Apr-21 60 | 61 | * Add support for Erlang 17.0 maps. Add a configure check for 62 | maps (don't test maps on pre-17.0) 63 | * Move some tests from examples/ to test/ and add a top-level 64 | Makefile target: test 65 | * Add configure checks for eqc[mini] and proper, and run those 66 | tests only if any of them is present. 67 | 68 | version 1.1.1, 2010-Mar-15 69 | 70 | * examples/run_remote_exec_wrapper.sh, 71 | examples/test_remote_exec.py: Really do include the files that 72 | were expected to be new as of version 1.1. 73 | 74 | * examples/run_test_erl_node_pingpong_qc.sh, 75 | examples/test_erl_node_pingpong_qc.erl: Really do include the 76 | files that were expected to be new as of version 1.1. 77 | 78 | * ChangeLog: include this file in the tar ball 79 | 80 | version 1.1, 2010-Feb-22 81 | 82 | * py_interface/erl_node_conn.py: Bugfix: now handles incoming 83 | packets larger than 64kbytes. 84 | * examples/run_test_erl_node_pingpong_qc.sh, 85 | examples/test_erl_node_pingpong_qc.erl: QuickCheck test of 86 | pingpong sending of data. 87 | * examples/run_test_erl_node_pingpong.sh: possibility to 88 | parameterize what erl command to use for easier testing of 89 | different version. 90 | 91 | * examples/run_remote_exec_wrapper.sh, 92 | examples/test_remote_exec.py: Implemented after a discussion 93 | with HP Wei. 94 | 95 | version 1.0, 2010-Apr-13 96 | * py_interface/erl_epmd.py: Erlang/OTP epmd R13B04 compatibility 97 | patch by Paul "TBBle" Hampson applied: R13B04 fixed a bug in the 98 | epmd's handling of the `extra' field. This patch makes 99 | py_interface work with the bug-fixed epmd. 100 | 101 | version 0.99, 2010-Mar-13 102 | * py_interface/erl_common.py: Bugfix of call to _HexDumpFormat in 103 | DebugHex. Reported by Paul "TBBle" Hampson. 104 | 105 | version 0.98, 2009-Nov-18 106 | * py_interface/erl_term.py: Bugfixes for handling of negative 107 | integers. Reported by bird devdoer . 108 | 109 | version 0.97, 2009-Jul-27 110 | * py_interface/erl_eventhandler.py: handles interrupted system 111 | call exceptions also in the timeout case. 112 | * py_interface/erl_node.py: doc string fix for methods 113 | ErlMBox.Send and ErlNode.SendMsgFromMBox: the order of the process 114 | name and node name had gotten mixed up. Noted by Paul TBBle 115 | Hampson. 116 | * py_interface/erl_node_conn.py: Python 2.6 compatibility patch by 117 | David Reiss applied: uses md5 from hashlib 118 | if that one is available. 119 | 120 | version 0.96, 2008-Dec-17 121 | Applied two patches from Anton Krasovsky: 122 | * The first fixes a crash when the connection from python to 123 | erlang node is terminated, because the erlang node has gone down 124 | or stopped replying to pings 125 | * The second fixes a crash when a connection between python node 126 | and the erlang node has been broken due to lack of ping 127 | responses from the erlang node (erlang process was suspended), 128 | and then when erlang process is resumed and terminated, 129 | exception was thrown. 130 | 131 | Fixed a few cases of the version number not getting properly 132 | inserted into all files, and one bug related to buildning: now all 133 | files needed to build gets properly included in the distribution 134 | tar file. 135 | 136 | version 0.95, 2008-Jul-15 137 | Applied patch from David King: 138 | * The socket connections now also checks for EWOULDBLOCK, not only 139 | EAGAIN. 140 | 141 | version 0.94, 2008-Jul-13 142 | Applied patches from David King: 143 | * The code now resides in its own namespace: py_interface 144 | * Now supports packaging as an easy_install egg 145 | * Bugfix: Queued writes attempted to access an undefined variable. 146 | 147 | version 0.93, 2006-Jul-25 148 | * Updated to work with Erlang R10 and R11 (15-->28 bits in pids 149 | and ports). 150 | * Now also works with threaded Tcl (as with Debian) as well. 151 | * Also fixed a deprecation issue: integer vs floating-point 152 | timeout values in erl_event_handler.py. 153 | * Also added an installation support (the __init__.py file), based 154 | on information by Ed Blake. 155 | 156 | version 0.92, 2004-Jul-13 157 | * Several bugfixes in packing/unpacking of erlang terms. A lot of 158 | thanks to Jimmy Olgeni and Nigel Head for patches. See the file 159 | erl_term.py, in the tar file, for details. 160 | 161 | version 0.91, 2002-Jul-21 162 | * Added autoconf stuff. 163 | * This version is/was also on the Erlang User contribution page 164 | 165 | version 0.9, 2002-May-29 166 | * First version 167 | -------------------------------------------------------------------------------- /Makefile.in: -------------------------------------------------------------------------------- 1 | ## This is a -*- makefile -*- 2 | 3 | # What the Python binary is called on your system 4 | PYTHON = @PYTHON@ 5 | 6 | 7 | # Prefix for constructing installation directory paths 8 | prefix = @prefix@ 9 | exec_prefix = $(prefix) 10 | 11 | # Installation command 12 | INSTALL = @INSTALL@ 13 | INSTALL_DATA = @INSTALL_DATA@ 14 | 15 | # Various auxiliary programs 16 | cat=cat 17 | tar=tar 18 | sed=sed 19 | rm=rm 20 | ln=ln 21 | mkdir=mkdir 22 | tar=tar 23 | chmod=chmod 24 | 25 | srcdir = @srcdir@ 26 | VPATH = @srcdir@ 27 | ERLC = @ERLC@ 28 | 29 | SOURCES = py_interface/erl_node.py \ 30 | py_interface/erl_node_conn.py \ 31 | py_interface/erl_async_conn.py \ 32 | py_interface/erl_opts.py \ 33 | py_interface/erl_common.py \ 34 | py_interface/erl_epmd.py \ 35 | py_interface/erl_eventhandler.py \ 36 | py_interface/erl_term.py 37 | 38 | TESTPROGRAMS = test/pingpong_master_tests.erl \ 39 | test/py_node_mgr.erl \ 40 | test/pingpong_slave.py \ 41 | test/out_connecting.py \ 42 | test/rpc_caller.py \ 43 | test/qc_pingpong_master_tests.erl \ 44 | test/test_erl_term.py \ 45 | test/Makefile.in 46 | 47 | 48 | OBJECTS = test/test_erl_node_pingpong.beam 49 | 50 | 51 | DISTFILES = $(SOURCES) $(TESTPROGRAMS) COPYING.LIB README \ 52 | py_interface/__init__.py py_interface/__init__.py.src \ 53 | vsn \ 54 | PKG-INFO PKG-INFO.src setup.py setup.py.src \ 55 | Makefile.in configure configure.in \ 56 | mkinstalldirs install-sh ChangeLog 57 | 58 | 59 | SHELL = /bin/sh 60 | #.PHONY: all clean dist distclean install \ 61 | # installdirs ps uninstall 62 | .SUFFIXES: .beam .erl .py 63 | 64 | all: py_interface/__init__.py setup.py PKG-INFO 65 | 66 | py_interface/__init__.py: py_interface/__init__.py.src 67 | version=`$(cat) vsn`; \ 68 | $(sed) -e "s/@VSN@/$$version/g" \ 69 | < py_interface/__init__.py.src \ 70 | > py_interface/__init__.py 71 | 72 | setup.py: setup.py.src 73 | version=`$(cat) vsn`; \ 74 | $(sed) -e "s/@VSN@/$$version/g" > setup.py < setup.py.src && \ 75 | $(chmod) +x setup.py 76 | 77 | PKG-INFO: PKG-INFO.src 78 | version=`$(cat) vsn`; \ 79 | $(sed) -e "s/@VSN@/$$version/g" > PKG-INFO < PKG-INFO.src 80 | 81 | 82 | install: all installdirs 83 | 84 | # Make sure all installation directories actually exist 85 | # by making them if necessary. 86 | installdirs: mkinstalldirs 87 | 88 | # $(srcdir)/mkinstalldirs $(pythondir) 89 | 90 | uninstall: 91 | 92 | # -cd $(pythondir) && rm -f $(SOURCES) $(OBJECTS) 93 | 94 | TAGS: $(SOURCES) 95 | cd $(srcdir) && etags $(SOURCES) 96 | 97 | clean: 98 | $(RM) $(OBJECTS) 99 | -$(RM) *.aux *.cp *.cps *.fn *.ky *.log *.pg *.toc *.tp *.vr 100 | -$(RM) *.html 101 | $(RM) TAGS 102 | $(RM) erl_crash.dump 103 | $(RM) *.beam 104 | $(RM) py_interface/__init__.py setup.py PKG-INFO 105 | find . -name \*.pyc -print0 | xargs -0 --no-run-if-empty $(RM) 106 | find . -name \*.pyo -print0 | xargs -0 --no-run-if-empty $(RM) 107 | $(MAKE) -C test $@ 108 | 109 | distclean: clean 110 | -$(RM) *~ *.tar.gz 111 | $(RM) Makefile config.status config.cache config.log 112 | 113 | ${srcdir}/configure: configure.in 114 | cd ${srcdir} && autoconf 115 | 116 | Makefile: Makefile.in config.status 117 | ./config.status 118 | 119 | config.status: ${srcdir}/configure 120 | ./config.status --recheck 121 | 122 | 123 | dist: $(DISTFILES) 124 | version=`$(cat) vsn`; \ 125 | distname=py_interface-$$version; \ 126 | $(rm) -rf $$distname; \ 127 | $(mkdir) $$distname; \ 128 | for file in $(DISTFILES); do \ 129 | d="`dirname $$file`"; \ 130 | [ -d "$$distname/$$d" ] || mkdir -p "$$distname/$$d"; \ 131 | $(ln) $$file $$distname/$$file; \ 132 | done; \ 133 | $(tar) -chz -f $$distname.tar.gz $$distname; \ 134 | /bin/rm -f $$distname.tgz; ln $$distname.tar.gz $$distname.tgz; \ 135 | $(rm) -rf $$distname 136 | 137 | .PHONY: test 138 | test: all 139 | $(MAKE) -C test $@ 140 | -------------------------------------------------------------------------------- /PKG-INFO.src: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: py_interface 3 | Version: @VSN@ 4 | Summary: A python-implementation of an Erlang node 5 | Home-page: http://www.lysator.liu.se/~tab/erlang/py_interface/ 6 | Author: Tomas Abrahamsson 7 | Author-email: tab@lysator.liu.se 8 | License: GNU Library General Public License 9 | Description: UNKNOWN 10 | Platform: UNKNOWN 11 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README for the py_interface 2 | =========================== 3 | 4 | What is py_interface 5 | ==================== 6 | 7 | The py_interface is a python-implementation of an Erlang node. For 8 | information on the Erlang programming language, visit the web site 9 | http://www.erlang.org/. 10 | 11 | Python 3 vs Python 2 12 | ==================== 13 | 14 | The py_interface as of version 2.0 supports only Python 3. 15 | For Python 2, use 1.x versions of py_interface. 16 | 17 | The intention for the 1.x is to only do bug fixes and minor work. 18 | Major efforts will target 2.x. 19 | 20 | Overview 21 | ======== 22 | 23 | The py_interface provides the possibility to create a node that may be 24 | used for communication with other Erlang nodes. 25 | 26 | Some characteristics: 27 | * The Python nodes are hidden, like the Java nodes 28 | * The Python node supports 29 | - registering the Python node in the epmd 30 | - sending and receiving message 31 | - executing remote procedure calls (the rpc:call(M,F,A) mechanism) 32 | * The Python node does currently _not_ do: 33 | - linking 34 | - tracing 35 | * The Python node translates Erlang types to Python types as far as 36 | there is a reasonable Python counterpart. If there is not, then 37 | a class is used. 38 | * The Python node is a single threaded callback-driven process. 39 | * The Python node is released under LGPL, see the file COPYING.LIB. 40 | * Work originally begain with Erlang R7 and Python 1.5 41 | * It was recently tested with Erlang 19.2 as well as R15B03 and Python 3.5 42 | * The source of information for this Python node has been the files 43 | `distribution_handshake.txt' and `erl_ext_dist.txt' together with 44 | the Java node source files, the `net_kernel.erl' and the 45 | `dist_util.erl' files in the Erlang source code distribution. 46 | Nowadays, this is all documented here: 47 | http://erlang.org/doc/apps/erts/erl_ext_dist.html 48 | http://erlang.org/doc/apps/erts/erl_dist_protocol.html 49 | 50 | 51 | General programming model 52 | ========================= 53 | 54 | When using the py_interface, the general principle is to register a 55 | callback for different purposes, such as incoming messages to the pid, 56 | an return from an rpc-call, or a timeout. The callback will get called 57 | whenever the message or return value arrives or when the timer times 58 | out. 59 | 60 | Erlang types vs Python types 61 | ============================ 62 | 63 | Erlang type Corresponding Python type 64 | ------------------------------------------------------------------ 65 | int int 66 | float float 67 | atom erl_term.ErlAtom instance 68 | tuple tuple 69 | 70 | list list or str or erl_term.ErlImproperList, 71 | see below for more info 72 | 73 | map erl_term.ErlMap instance, which implements a custom 74 | dictionary type, see below for more info 75 | 76 | pid erl_term.ErlPid instance 77 | binary bytes 78 | bitstring erl_term.ErlBitBinary instance 79 | 80 | fun erl_term.ErlFun or erl_term.ErlFunExport instance 81 | reference erl_term.ErlRef instance 82 | port erl_term.ErlPort instance 83 | 84 | 85 | About strings and types for data transfer Erlang --> Python 86 | ----------------------------------------------------------- 87 | 88 | In Erlang, there are no strings, only lists of integers, such as lists of 89 | Unicode code-points. When the Erlang side serializes a list of integers, 90 | it may encode it as STRING, if it looks like it could be a string. But it 91 | may also be serialized as a list of integers, depending on its content. 92 | But the heuristic may result in both false positives and false negatives. 93 | 94 | If the Erlang side has guessed that the list of integers may in fact be a 95 | string, it will be unpacked as a Python string. If it has been unpacked as 96 | a list of integers, the function erl_term.IODataToStr can be used to turn 97 | it into a string. A string can be turned into a list using the function 98 | erl_term.StrToList. 99 | 100 | About Erlang maps and Python dicts 101 | ---------------------------------- 102 | 103 | In Erlang, any value can be a key in a map. In Python, for the corresponding 104 | dict type, only immutable values can be keys; for example lists and class 105 | instances are mutable and thus cannot be used as keys in ordinary dicts. 106 | 107 | The way this is done in the py_interface ErlMap, which imitates a dict, is 108 | that for mutable keys, such as lists or ErlAtom instances, a hash of the 109 | serialized value is calculated upon insertion. Please do not change such 110 | mutable objects used as ErlMap keys, after insertion. Treat them as if 111 | they had been immutable, or the ErlMap probably won't work as expected. 112 | 113 | One more difference between the ErlMap and a normal Python dictionary is 114 | that in ErlMap, 0 and 0.0 are two different keys --- because that is how it 115 | is for Erlang maps, and it is desirable to preserve round-trip consistency 116 | for all terms. For normal Python dicts, 0 and 0.0 is the same key. 117 | 118 | 119 | Examples 120 | ======== 121 | 122 | The easiest way to get acquainted with the Python node, run test programs 123 | interactively in two terminal windows: 124 | 125 | Example 1: A connection from and Erlang node to a Python node 126 | ------------------------------------------------------------- 127 | 128 | We will create a Python node named test@127.0.0.1, with cookie x, 129 | it will in turn create and register a process with the name p. 130 | Then it will wait for messages, presumably on the form {Pid::pid(),Msg} 131 | and if it has this format, it will send {self(),Msg} back to Pid. 132 | 133 | We will then start an Erlang node which sends such a message and waits 134 | for something to be come back. 135 | 136 | 137 | In window 1: 138 | 139 | $ cd test 140 | $ ./pingpong_slave.py -d -n test@127.0.0.1 -c x 141 | 142 | 143 | In window 2: 144 | 145 | $ erl -name x@127.0.0.1 -setcookie x \ 146 | -eval "{p,'test@127.0.0.1'}"' ! {self(),hello}, receive X -> io:format("Got ~p~n", [X]), halt() end.' 147 | 148 | 149 | Example 2: An outgoing connection from a Python node to an Erlang node 150 | ---------------------------------------------------------------------- 151 | 152 | We will make an outgoing connection from a Python node to a named 153 | process in an Erlang node. The Erlang side registers a process with name 154 | ppp in a node with name e@127.0.0.1. The Python side connects to this, 155 | and sends a message, the atom mmm. 156 | 157 | In window 1: 158 | 159 | $ erl -name e@127.0.0.1 -setcookie x \ 160 | -eval 'register(ppp,self()), receive X -> io:format("Got ~p~n", [X]), halt() end.' 161 | 162 | In window 2: 163 | 164 | $ cd test 165 | $ ./out_connecting.py -d -n t@127.0.0.1 -c x ppp e@127.0.0.1 mmm 166 | 167 | Example 3: Make a remote procedure call (rpc) from Python to Erlang 168 | ---------------------------------------------------------------------- 169 | 170 | We will call a function in the Erlang node from the Python side. 171 | The '[[1,2,3]]' is a list of one argument, the list to sum. The single 172 | quotes are to protect it from being interpreted by the shell that we 173 | start it from. 174 | 175 | In window 1: 176 | 177 | $ erl -name e@127.0.0.1 -setcookie x 178 | 179 | In window 2: 180 | 181 | $ cd test 182 | $ rpc_caller.py -d -n t@127.0.0.1 -c x e@127.0.0.1 lists sum '[[1,2,3]]' 183 | 184 | 185 | Contacting the author 186 | ===================== 187 | To contact the author, Tomas Abrahamsson: send a mail to: 188 | tab@lysator.liu.se or tomas.abrahamsson@gmail.com 189 | 190 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Cookies: Make it read the first line of ~/.erlang.cookie, trim whitespace 2 | 3 | Nodename: Check whether we should use long or short node name 4 | e.g. @host.domain.com or just @host 5 | Nodename: must not exceed 255 characters! 6 | -------------------------------------------------------------------------------- /configure.in: -------------------------------------------------------------------------------- 1 | dnl Process this file with autoconf to produce a configure script. 2 | AC_INIT() 3 | 4 | AC_PROG_INSTALL() 5 | 6 | AC_CHECK_PROGS(PYTHON, python) 7 | 8 | AC_CHECK_PROGS(ERL, erl) 9 | AC_CHECK_PROGS(ERLC, erlc) 10 | 11 | silent_erl_flags="+B -noinput -sasl errlog_type error" 12 | 13 | AC_MSG_CHECKING([whether Erlang supports maps]) 14 | if (ERL_CRASH_DUMP_SECONDS=0 ${ERL} ${silent_erl_flags} \ 15 | -eval '#{a=>1},halt(0).' \ 16 | >/dev/null 2>&1) 17 | then 18 | HAVE_MAPS=yes 19 | else 20 | HAVE_MAPS=no 21 | fi 22 | AC_MSG_RESULT($HAVE_MAPS) 23 | AC_SUBST(HAVE_MAPS) 24 | 25 | AC_MSG_CHECKING([whether Erlang has erts7 time api]) 26 | if (ERL_CRASH_DUMP_SECONDS=0 ${ERL} ${silent_erl_flags} \ 27 | -eval 'try erlang:monotonic_time(), halt(0) 28 | catch error:undef -> halt(1) 29 | end.' \ 30 | >/dev/null 2>&1) 31 | then 32 | HAVE_ERTS7_TIME_API=yes 33 | else 34 | HAVE_ERTS7_TIME_API=no 35 | fi 36 | AC_MSG_RESULT($HAVE_ERTS7_TIME_API) 37 | AC_SUBST(HAVE_ERTS7_TIME_API) 38 | 39 | AC_MSG_CHECKING([for eqc]) 40 | if (ERL_CRASH_DUMP_SECONDS=0 ${ERL} ${silent_erl_flags} \ 41 | -eval \ 42 | 'case catch code:which(eqc) of 43 | Path when is_list(Path) -> halt(0); 44 | _ -> halt(1) 45 | end.' \ 46 | >/dev/null 2>&1) 47 | then 48 | dnl in the io:format below, [[ is translated by m4 into a single [ 49 | HAVE_EQC=yes 50 | eqc_loc=`ERL_CRASH_DUMP_SECONDS=0 ${ERL} ${silent_erl_flags} \ 51 | -eval \ 52 | 'io:format("~s~n",[[code:which(eqc)]]),halt().'` 53 | else 54 | HAVE_EQC=no 55 | eqc_loc=no 56 | fi 57 | AC_MSG_RESULT($eqc_loc) 58 | AC_SUBST(HAVE_EQC) 59 | 60 | AC_MSG_CHECKING([for proper]) 61 | if (ERL_CRASH_DUMP_SECONDS=0 ${ERL} ${silent_erl_flags} \ 62 | -eval \ 63 | 'case catch code:which(proper) of 64 | Path when is_list(Path) -> halt(0); 65 | _ -> halt(1) 66 | end.' \ 67 | >/dev/null 2>&1) 68 | then 69 | dnl in the io:format below, [[ is translated by m4 into a single [ 70 | HAVE_PROPER=yes 71 | proper_loc=`ERL_CRASH_DUMP_SECONDS=0 ${ERL} ${silent_erl_flags} \ 72 | -eval \ 73 | 'io:format("~s~n",[[code:which(proper)]]),halt().'` 74 | else 75 | HAVE_PROPER=no 76 | proper_loc=no 77 | fi 78 | AC_MSG_RESULT($proper_loc) 79 | AC_SUBST(HAVE_PROPER) 80 | 81 | AC_OUTPUT(Makefile) 82 | AC_OUTPUT(test/Makefile) 83 | -------------------------------------------------------------------------------- /howto-make-a-new-version: -------------------------------------------------------------------------------- 1 | #! /bin/sh -xv 2 | # 1. Edit 3 | # 2. Edit the file vsn 4 | # 3. Edit the file ChangeLog 5 | # 4. Test: 6 | autoconf && \ 7 | ./configure && \ 8 | make clean && \ 9 | make && \ 10 | ./setup.py build && \ 11 | make test && \ 12 | make dist && \ 13 | ( export vsn=`cat vsn` && \ 14 | /bin/rm -rf tt && \ 15 | mkdir tt && cd tt && \ 16 | tar xfvpz ../py_interface-$vsn.tgz && \ 17 | cd py_interface-$vsn && \ 18 | ./configure && \ 19 | make && \ 20 | make test && \ 21 | ./setup.py build ) && \ 22 | /bin/rm -rf tt && 23 | echo "Success!" 24 | # 5. Commit 25 | # 6. Tag 26 | # 7. Push 27 | -------------------------------------------------------------------------------- /install-sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # 3 | # install - install a program, script, or datafile 4 | # This comes from X11R5. 5 | # 6 | # Calling this script install-sh is preferred over install.sh, to prevent 7 | # `make' implicit rules from creating a file called install from it 8 | # when there is no Makefile. 9 | # 10 | # This script is compatible with the BSD install script, but was written 11 | # from scratch. 12 | # 13 | 14 | 15 | # set DOITPROG to echo to test this script 16 | 17 | # Don't use :- since 4.3BSD and earlier shells don't like it. 18 | doit="${DOITPROG-}" 19 | 20 | 21 | # put in absolute paths if you don't have them in your path; or use env. vars. 22 | 23 | mvprog="${MVPROG-mv}" 24 | cpprog="${CPPROG-cp}" 25 | chmodprog="${CHMODPROG-chmod}" 26 | chownprog="${CHOWNPROG-chown}" 27 | chgrpprog="${CHGRPPROG-chgrp}" 28 | stripprog="${STRIPPROG-strip}" 29 | rmprog="${RMPROG-rm}" 30 | mkdirprog="${MKDIRPROG-mkdir}" 31 | 32 | tranformbasename="" 33 | transform_arg="" 34 | instcmd="$mvprog" 35 | chmodcmd="$chmodprog 0755" 36 | chowncmd="" 37 | chgrpcmd="" 38 | stripcmd="" 39 | rmcmd="$rmprog -f" 40 | mvcmd="$mvprog" 41 | src="" 42 | dst="" 43 | dir_arg="" 44 | 45 | while [ x"$1" != x ]; do 46 | case $1 in 47 | -c) instcmd="$cpprog" 48 | shift 49 | continue;; 50 | 51 | -d) dir_arg=true 52 | shift 53 | continue;; 54 | 55 | -m) chmodcmd="$chmodprog $2" 56 | shift 57 | shift 58 | continue;; 59 | 60 | -o) chowncmd="$chownprog $2" 61 | shift 62 | shift 63 | continue;; 64 | 65 | -g) chgrpcmd="$chgrpprog $2" 66 | shift 67 | shift 68 | continue;; 69 | 70 | -s) stripcmd="$stripprog" 71 | shift 72 | continue;; 73 | 74 | -t=*) transformarg=`echo $1 | sed 's/-t=//'` 75 | shift 76 | continue;; 77 | 78 | -b=*) transformbasename=`echo $1 | sed 's/-b=//'` 79 | shift 80 | continue;; 81 | 82 | *) if [ x"$src" = x ] 83 | then 84 | src=$1 85 | else 86 | # this colon is to work around a 386BSD /bin/sh bug 87 | : 88 | dst=$1 89 | fi 90 | shift 91 | continue;; 92 | esac 93 | done 94 | 95 | if [ x"$src" = x ] 96 | then 97 | echo "install: no input file specified" 98 | exit 1 99 | else 100 | true 101 | fi 102 | 103 | if [ x"$dir_arg" != x ]; then 104 | dst=$src 105 | src="" 106 | 107 | if [ -d $dst ]; then 108 | instcmd=: 109 | else 110 | instcmd=mkdir 111 | fi 112 | else 113 | 114 | # Waiting for this to be detected by the "$instcmd $src $dsttmp" command 115 | # might cause directories to be created, which would be especially bad 116 | # if $src (and thus $dsttmp) contains '*'. 117 | 118 | if [ -f $src -o -d $src ] 119 | then 120 | true 121 | else 122 | echo "install: $src does not exist" 123 | exit 1 124 | fi 125 | 126 | if [ x"$dst" = x ] 127 | then 128 | echo "install: no destination specified" 129 | exit 1 130 | else 131 | true 132 | fi 133 | 134 | # If destination is a directory, append the input filename; if your system 135 | # does not like double slashes in filenames, you may need to add some logic 136 | 137 | if [ -d $dst ] 138 | then 139 | dst="$dst"/`basename $src` 140 | else 141 | true 142 | fi 143 | fi 144 | 145 | ## this sed command emulates the dirname command 146 | dstdir=`echo $dst | sed -e 's,[^/]*$,,;s,/$,,;s,^$,.,'` 147 | 148 | # Make sure that the destination directory exists. 149 | # this part is taken from Noah Friedman's mkinstalldirs script 150 | 151 | # Skip lots of stat calls in the usual case. 152 | if [ ! -d "$dstdir" ]; then 153 | defaultIFS=' 154 | ' 155 | IFS="${IFS-${defaultIFS}}" 156 | 157 | oIFS="${IFS}" 158 | # Some sh's can't handle IFS=/ for some reason. 159 | IFS='%' 160 | set - `echo ${dstdir} | sed -e 's@/@%@g' -e 's@^%@/@'` 161 | IFS="${oIFS}" 162 | 163 | pathcomp='' 164 | 165 | while [ $# -ne 0 ] ; do 166 | pathcomp="${pathcomp}${1}" 167 | shift 168 | 169 | if [ ! -d "${pathcomp}" ] ; 170 | then 171 | $mkdirprog "${pathcomp}" 172 | else 173 | true 174 | fi 175 | 176 | pathcomp="${pathcomp}/" 177 | done 178 | fi 179 | 180 | if [ x"$dir_arg" != x ] 181 | then 182 | $doit $instcmd $dst && 183 | 184 | if [ x"$chowncmd" != x ]; then $doit $chowncmd $dst; else true ; fi && 185 | if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dst; else true ; fi && 186 | if [ x"$stripcmd" != x ]; then $doit $stripcmd $dst; else true ; fi && 187 | if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dst; else true ; fi 188 | else 189 | 190 | # If we're going to rename the final executable, determine the name now. 191 | 192 | if [ x"$transformarg" = x ] 193 | then 194 | dstfile=`basename $dst` 195 | else 196 | dstfile=`basename $dst $transformbasename | 197 | sed $transformarg`$transformbasename 198 | fi 199 | 200 | # don't allow the sed command to completely eliminate the filename 201 | 202 | if [ x"$dstfile" = x ] 203 | then 204 | dstfile=`basename $dst` 205 | else 206 | true 207 | fi 208 | 209 | # Make a temp file name in the proper directory. 210 | 211 | dsttmp=$dstdir/#inst.$$# 212 | 213 | # Move or copy the file name to the temp name 214 | 215 | $doit $instcmd $src $dsttmp && 216 | 217 | trap "rm -f ${dsttmp}" 0 && 218 | 219 | # and set any options; do chmod last to preserve setuid bits 220 | 221 | # If any of these fail, we abort the whole thing. If we want to 222 | # ignore errors from any of these, just make sure not to ignore 223 | # errors from the above "$doit $instcmd $src $dsttmp" command. 224 | 225 | if [ x"$chowncmd" != x ]; then $doit $chowncmd $dsttmp; else true;fi && 226 | if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dsttmp; else true;fi && 227 | if [ x"$stripcmd" != x ]; then $doit $stripcmd $dsttmp; else true;fi && 228 | if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dsttmp; else true;fi && 229 | 230 | # Now rename the file to the real destination. 231 | 232 | $doit $rmcmd -f $dstdir/$dstfile && 233 | $doit $mvcmd $dsttmp $dstdir/$dstfile 234 | 235 | fi && 236 | 237 | 238 | exit 0 239 | -------------------------------------------------------------------------------- /mkinstalldirs: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # mkinstalldirs --- make directory hierarchy 3 | # Author: Noah Friedman 4 | # Created: 1993-05-16 5 | # Last modified: 1994-03-25 6 | # Public domain 7 | 8 | errstatus=0 9 | 10 | for file in ${1+"$@"} ; do 11 | set fnord `echo ":$file" | sed -ne 's/^:\//#/;s/^://;s/\// /g;s/^#/\//;p'` 12 | shift 13 | 14 | pathcomp= 15 | for d in ${1+"$@"} ; do 16 | pathcomp="$pathcomp$d" 17 | case "$pathcomp" in 18 | -* ) pathcomp=./$pathcomp ;; 19 | esac 20 | 21 | if test ! -d "$pathcomp"; then 22 | echo "mkdir $pathcomp" 1>&2 23 | mkdir "$pathcomp" || errstatus=$? 24 | fi 25 | 26 | pathcomp="$pathcomp/" 27 | done 28 | done 29 | 30 | exit $errstatus 31 | 32 | # mkinstalldirs ends here 33 | -------------------------------------------------------------------------------- /py_interface/__init__.py.src: -------------------------------------------------------------------------------- 1 | """This file lets the py_interface files function as a package.""" 2 | __version__ = "@VSN@" 3 | __author__ = "Tomas Abrahamsson" 4 | __license__ = "GNU Library General Public License" 5 | __all__ = [ 6 | "erl_async_conn", 7 | "erl_common", 8 | "erl_epmd", 9 | "erl_eventhandler", 10 | "erl_node", 11 | "erl_node_conn", 12 | "erl_opts", 13 | "erl_term" 14 | ] 15 | -------------------------------------------------------------------------------- /py_interface/erl_async_conn.py: -------------------------------------------------------------------------------- 1 | ### py_interface -- A Python-implementation of an Erlang node 2 | ### 3 | ### $Id$ 4 | ### 5 | ### Copyright (C) 2002 Tomas Abrahamsson 6 | ### 7 | ### Author: Tomas Abrahamsson 8 | ### 9 | ### This file is part of the Py-Interface library 10 | ### 11 | ### This library is free software; you can redistribute it and/or 12 | ### modify it under the terms of the GNU Library General Public 13 | ### License as published by the Free Software Foundation; either 14 | ### version 2 of the License, or (at your option) any later version. 15 | ### 16 | ### This library is distributed in the hope that it will be useful, 17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | ### Library General Public License for more details. 20 | ### 21 | ### You should have received a copy of the GNU Library General Public 22 | ### License along with this library; if not, write to the Free 23 | ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | ### erl_async_conn.py -- general async communication over tcp/ip 26 | 27 | import socket 28 | try: 29 | import SOCKET 30 | except ImportError: 31 | SOCKET = socket 32 | import errno 33 | 34 | from py_interface import erl_common 35 | from py_interface import erl_eventhandler 36 | 37 | 38 | class ErlAsyncPeerConnection: 39 | def __init__(self, openSocket=None): 40 | self.evhandler = erl_eventhandler.GetEventHandler() 41 | if openSocket == None: 42 | self._Init() 43 | else: 44 | self._Init() 45 | self._SetConnectionOpen(openSocket) 46 | 47 | 48 | def Close(self): 49 | if not self._isConnected: 50 | return 51 | self._SetConnectionClosed() 52 | 53 | 54 | def Send(self, data): 55 | self._SendOrQueue(data) 56 | 57 | 58 | def GetConnection(self): 59 | if not self._isConnected: 60 | return None 61 | else: 62 | return self._connection 63 | 64 | 65 | ## 66 | ## Internal routines 67 | ## 68 | 69 | def _Init(self): 70 | self._isConnected = 0 71 | self._pendingOutput = b"" 72 | 73 | def _SetConnectionClosed(self): 74 | if self._isConnected: 75 | self.evhandler.PopReadEvent(self._connection) 76 | self._connection.close() 77 | self._connection = None 78 | self._isConnected = 0 79 | self._Init() 80 | 81 | def _SetConnectionOpen(self, sock): 82 | if not self._isConnected: 83 | self._connection = sock 84 | self._isConnected = 1 85 | self.evhandler.PushReadEvent(self._connection, self._In) 86 | 87 | def _SendOrQueue(self, data): 88 | numBytesToSend = len(data) 89 | try: 90 | numBytesSent = self._connection.send(data) 91 | if numBytesSent < numBytesToSend: 92 | remaining = data[numBytesSent:] 93 | self._Queue(remaining) 94 | except socket.error as err: 95 | errNum = err.args[0] 96 | if errNum == errno.EAGAIN or errNum == errno.EWOULDBLOCK: 97 | self._Queue(data) 98 | else: 99 | raise 100 | 101 | def _Queue(self, strToQueue): 102 | if self._pendingOutput == b"": 103 | self.evhandler.PushWriteEvent(self._connection, self._QueuedWrite) 104 | self._pendingOutput = self._pendingOutput + strToQueue 105 | 106 | def _QueuedWrite(self): 107 | numBytesToSend = len(self._pendingOutput) 108 | try: 109 | numBytesSent = self._connection.send(self._pendingOutput) 110 | if numBytesSent == numBytesToSend: 111 | self._pendingOutput = b"" 112 | self.evhandler.PopWriteEvent(self._connection) 113 | else: 114 | self._pendingOutput = self._pendingOutput[numBytesSent:] 115 | except socket.error as err: 116 | errNum = err.args[0] 117 | if errNum == errno.EAGAIN or errNum == errno.EWOULDBLOCK: 118 | # still not possible to send... 119 | # wait a bit more 120 | pass 121 | 122 | def _In(self): 123 | print("erl_sync_conn: please override me!") 124 | newData = self._connection.recv(10000) 125 | 126 | def ReadInt1(self, s): 127 | return erl_common.ReadInt1(s) 128 | 129 | def ReadInt2(self, s): 130 | return erl_common.ReadInt2(s) 131 | 132 | def ReadInt4(self, s): 133 | return erl_common.ReadInt4(s) 134 | 135 | def PackInt1(self, i): 136 | return erl_common.PackInt1(i) 137 | 138 | def PackInt2(self, i): 139 | return erl_common.PackInt2(i) 140 | 141 | def PackInt4(self, i): 142 | return erl_common.PackInt4(i) 143 | 144 | 145 | class ErlAsyncClientConnection(ErlAsyncPeerConnection): 146 | def __init__(self): 147 | ErlAsyncPeerConnection.__init__(self) 148 | 149 | 150 | def Connect(self, hostname, portNum): 151 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 152 | try: 153 | s.connect((hostname, portNum)) 154 | s.setblocking(0) 155 | self.hostname = hostname 156 | self.portNum = portNum 157 | self._SetConnectionOpen(s) 158 | return 1 159 | except socket.error as errInfo: 160 | print("socket error:", errInfo) 161 | self._SetConnectionClosed() 162 | return 0 163 | 164 | 165 | class ErlAsyncServer(ErlAsyncPeerConnection): 166 | def __init__(self): 167 | ErlAsyncPeerConnection.__init__(self) 168 | 169 | def Start(self, portNum=0, iface=""): 170 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 171 | try: 172 | s.setsockopt(SOCKET.SOL_SOCKET, SOCKET.SO_REUSEADDR, 1) 173 | s.bind((iface, portNum)) 174 | (ipNum, resultPortNum) = s.getsockname() 175 | s.listen(5) 176 | s.setblocking(0) 177 | self.hostname = iface 178 | self.portNum = resultPortNum 179 | self._SetConnectionOpen(s) 180 | return resultPortNum 181 | except socket.error as errInfo: 182 | print("socket error:", errInfo) 183 | return None 184 | 185 | def _In(self): 186 | s = self.GetConnection() 187 | (s2, (remoteHost, remotePort)) = s.accept() 188 | self._NewConnection(s2, (remoteHost, remotePort)) 189 | 190 | def _NewConnection(self, sockConn, remoteAddr): 191 | asyncConnection = ErlAsyncPeerConnection(s2) 192 | pass 193 | -------------------------------------------------------------------------------- /py_interface/erl_common.py: -------------------------------------------------------------------------------- 1 | ### py_interface -- A Python-implementation of an Erlang node 2 | ### 3 | ### $Id$ 4 | ### 5 | ### Copyright (C) 2002 Tomas Abrahamsson 6 | ### 7 | ### Author: Tomas Abrahamsson 8 | ### 9 | ### This file is part of the Py-Interface library 10 | ### 11 | ### This library is free software; you can redistribute it and/or 12 | ### modify it under the terms of the GNU Library General Public 13 | ### License as published by the Free Software Foundation; either 14 | ### version 2 of the License, or (at your option) any later version. 15 | ### 16 | ### This library is distributed in the hope that it will be useful, 17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | ### Library General Public License for more details. 20 | ### 21 | ### You should have received a copy of the GNU Library General Public 22 | ### License along with this library; if not, write to the Free 23 | ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | ### erl_common.py -- common utility functions 26 | 27 | import os 28 | import string 29 | import socket 30 | 31 | def ReadInt1(s): 32 | """Convert first byte from a string to an unsigned 8-bit integer.""" 33 | if type(s) == int: 34 | return s 35 | elif type(s) == bytes: 36 | return s[0] 37 | 38 | def ReadInt2(s): 39 | """Convert first two bytes from a string to an unsigned 16-bit integer.""" 40 | return (s[0] << 8) + \ 41 | (s[1] << 0) 42 | 43 | def ReadInt4(s): 44 | """Convert first four bytes from a string to an unsigned 32-bit integer. 45 | 46 | Returns integer | long 47 | 48 | In Python on a 32-bit machine, integers are signed and within 49 | the range -2^31 .. (2^31 - 1). If the 32-bit integer fits within this 50 | range, an integer is returned, otherwise a long is returned.""" 51 | return (s[0] << 24) + \ 52 | (s[1] << 16) + \ 53 | (s[2] << 8) + \ 54 | (s[3] << 0) 55 | 56 | def PackInt1(i): 57 | """Converts an unsigned 8-bit integer/long into a string, 1 byte long.""" 58 | return bytes([i & 255]) 59 | 60 | def PackInt2(i): 61 | """Converts an unsigned 16-bit integer/long into a string, 2 byte long.""" 62 | return bytes([(i >> 8) & 255, 63 | (i >> 0) & 255]) 64 | 65 | def PackInt4(i): 66 | """Converts an unsigned 32-bit integer/long into a string, 4 byte long.""" 67 | return bytes([(i >> 24) & 255, 68 | (i >> 16) & 255, 69 | (i >> 8) & 255, 70 | (i >> 0) & 255]) 71 | 72 | def AlignNodeName(nodeName, useShortNodeNames=1): 73 | """Make sure the node name is a valid node name. 74 | 75 | NODE-NAME = string 76 | A node name, possibly containing an "@" 77 | USE-SHORT-NODE-NAMES = bool 78 | Whether to align using short node names 79 | or not. If not, then long node names are 80 | assumed. 81 | 82 | Returns: string 83 | Throws: nothing 84 | 85 | The returned node name: 86 | 87 | 1. If the NODE-NAME contains an `@', then NODE-NAME is returned unchanged 88 | 2. If the NODE-NAME does not contain an `@', then the returned node name 89 | is constructed as NODE-NAME + "@" + host-name, where 90 | host-name is either on short or long form, depending on 91 | USE-SHORT-NODE-NAMES. 92 | """ 93 | if useShortNodeNames: 94 | return AlignShortNodeName(nodeName) 95 | else: 96 | return AlignLongNodeName(nodeName) 97 | 98 | def AlignShortNodeName(nodeName): 99 | """Align a node, use short hostname if needed. See doc for AlignNodeName""" 100 | if "@" in nodeName: 101 | return nodeName 102 | fqdn = GetFullyQualifiedHostName() 103 | shortHostName = fqdn.split(".")[0] 104 | return nodeName + "@" + shortHostName 105 | 106 | def AlignLongNodeName(nodeName): 107 | """Align a node, use long hostname if needed. See doc for AlignNodeName""" 108 | if "@" in nodeName: 109 | return nodeName 110 | fqdn = GetFullyQualifiedHostName() 111 | return nodeName + "@" + fqdn 112 | 113 | def GetFullyQualifiedHostName(name=""): 114 | """Get fully qualified domain name from name. 115 | 116 | An empty argument is interpreted as meaning the local host. 117 | 118 | First the hostname returned by gethostbyaddr() is checked, then 119 | possibly existing aliases. In case no FQDN is available, hostname 120 | is returned. 121 | """ 122 | try: 123 | # Try the builtin version if it exists. 124 | # It does in Python 2.0 125 | return socket.getfqdn(name) 126 | except AttributeError as info: 127 | # This fallback code is provided for Python 1.5.2 and earlier 128 | # It is stolen with pride from Python 2.0 129 | name = name.strip() 130 | if not name or name == '0.0.0.0': 131 | name = socket.gethostname() 132 | try: 133 | hostname, aliases, ipaddrs = socket.gethostbyaddr(name) 134 | except error: 135 | pass 136 | else: 137 | aliases.insert(0, hostname) 138 | for name in aliases: 139 | if '.' in name: 140 | break 141 | else: 142 | name = hostname 143 | return name 144 | 145 | def getenv(envName): 146 | """Read an environment variable. 147 | ENV-NAME = string 148 | The name of the environment variable 149 | Returns: "" | string 150 | The env-value, or "" if the env-name isn't defined 151 | Throws: nothing 152 | """ 153 | if envName in os.environ: 154 | return os.environ[envName] 155 | else: 156 | return "" 157 | 158 | 159 | 160 | def IndexSeq(seq): 161 | """Given a sequence, return a list of tuples (i, elem[i]). 162 | Example: IndexSeq(["a", "b", "c"]) ==> [(0, "a"), (1, "b"), (2, "c")].""" 163 | return zip(range(len(seq)), seq) 164 | 165 | 166 | _logfilename = None 167 | def SetLogFile(fileName): 168 | """Setup a file name to log to 169 | FILE-NAME = string 170 | The name of a file to log to 171 | Returns: void 172 | 173 | Sets up a log file. 174 | """ 175 | global _logfilename 176 | 177 | def Log(str): 178 | """Log a string to the log file 179 | STR = string 180 | The string to log (without trailing new-line) 181 | Returns: void 182 | Throws: nothing 183 | 184 | Logs a string to the file set up by logfilename. 185 | The file is opened only during the logging. It is kept closed between 186 | calls to this function. 187 | """ 188 | global _logfilename 189 | if _logfilename != None: 190 | try: 191 | f = open(_logfilename, "w") 192 | f.write(str) 193 | f.write("\n") 194 | f.flush() 195 | f.close() 196 | except IOError as info: 197 | # Silently ignore 198 | pass 199 | 200 | 201 | _modulesToDebug = [] 202 | _debugAllModules = 0 203 | _debugFileName = None 204 | 205 | def DebugToFile(fileName): 206 | """Setup a file name to print debugging messages to 207 | FILE-NAME = string 208 | The name of a file to log to 209 | Returns: void 210 | Throws: nothing 211 | 212 | Sets up a file to print debugging messages to. 213 | """ 214 | global _debugFileName 215 | _debugFileName = fileName 216 | 217 | def DebugOnAll(): 218 | """Turn on debugging for all modules 219 | No arguments 220 | Returns: void 221 | Throws: nothing 222 | 223 | Turns on debugging flag for all modules 224 | """ 225 | global _modulesToDebug, _debugAllModules 226 | _debugAllModules = 1 227 | 228 | 229 | def DebugOn(moduleList): 230 | """Turn on debugging for selected modules 231 | MODULE-LIST = list(string) 232 | 233 | Returns: void 234 | Throws: nothing 235 | 236 | Turns on debugging flag for selected modules 237 | """ 238 | global _modulesToDebug, _debugAllModules 239 | _debugAllModules = 0 240 | _modulesToDebug = moduleList 241 | 242 | def Debug(module, txt): 243 | """Print a debug text 244 | MODULE = string 245 | Name of the module that is printing the debug text 246 | TXT = string 247 | The text to print as debugging message 248 | Returns: void 249 | Throws: nothing 250 | 251 | Prints a debugging text. The text is printed if debugging for the 252 | module is turned on, see DebugOnAll and DebugOn. 253 | 254 | The debug message is printed to stdout, except if debugging has 255 | been set to go to a file, see DebugToFile. 256 | """ 257 | global _modulesToDebug, _debugAllModules 258 | if _debugAllModules or module in _modulesToDebug: 259 | _DebugEmitText("%s: %s" % (module, txt)) 260 | 261 | def DebugHex(module, txt, msg): 262 | """Print a debug text and a hexdump of a message 263 | MODULE = string 264 | Name of the module that is printing the debug text 265 | TXT = string 266 | The text to print as debugging message 267 | MSG = string 268 | The message to hexdump 269 | Returns: void 270 | Throws: nothing 271 | 272 | Prints a debugging text. The text is printed if debugging for the 273 | module is turned on, see DebugOnAll and DebugOn. 274 | 275 | The debug message is printed to stdout, except if debugging has 276 | been set to go to a file, see DebugToFile. 277 | """ 278 | hexMsg = _HexDumpFormat(msg) 279 | _DebugEmitText("%s: %s\n%s" % (module, txt, hexMsg)) 280 | 281 | def _DebugEmitText(txt): 282 | global _debugFileName 283 | if _debugFileName == None: 284 | print(txt) 285 | else: 286 | try: 287 | f = open(_debugFileName, "w") 288 | f.write(txt) 289 | f.flush() 290 | f.close() 291 | except IOError as info: 292 | # Silently ignore 293 | pass 294 | 295 | 296 | def _HexDumpFormat(string): 297 | def dump_chars(addr, s): 298 | hexString = "" 299 | ascString = "" 300 | for i, c in zip(range(len(s)), s): 301 | if i > 0: 302 | hexString = hexString + " " 303 | hexString = hexString + ("%02x" % c) 304 | if _IsNonAsciiPrintable(c): 305 | ascString = ascString + "." 306 | else: 307 | ascString = ascString + chr(c) 308 | numFill = 16 - len(s) 309 | hexString = hexString + " " * numFill 310 | addrString = "%04x" % addr 311 | return addrString + ": " + hexString + " " + ascString + "\n" 312 | 313 | remaining_chars = string; 314 | addr = 0 315 | result = "" 316 | while len(remaining_chars) > 0: 317 | if len(remaining_chars) < 16: 318 | result = result + dump_chars(addr, remaining_chars) 319 | remaining_chars = "" 320 | else: 321 | result = result + dump_chars(addr, remaining_chars[:16]) 322 | remaining_chars = remaining_chars[16:] 323 | addr = addr + 16 324 | return result 325 | 326 | def _IsNonAsciiPrintable(c): 327 | if type(c) == bytes: 328 | c = ord(c[0]) 329 | elif type(c) == str: 330 | c = ord(c[0]) 331 | return (c < ord(" ")) or ((127 <= c) and (c < 160)) 332 | 333 | class Callback: 334 | """This class provides a callback object. 335 | Here's how to use it: 336 | 337 | def MyFunction1(...): 338 | pass 339 | def MyFunction2(..., arg1, arg2, arg3): 340 | pass 341 | def MyFunction3(..., arg1, arg2, arg3, kw1=None, kw2=None): 342 | pass 343 | 344 | cb1 = Callback(MyFunction1) 345 | cb2 = Callback(MyFunction2, 1, 2, 3) 346 | cb3 = Callback(MyFunction3, 1, 2, 3, kw1=4, kw2=5) 347 | 348 | RegisterCallback(cb1) 349 | RegisterCallback(cb2) 350 | RegisterCallback(cb3) 351 | 352 | Any arguments that the invoker uses are tacked on before the first 353 | callback-argument, hence the `...' in the function definitions above. 354 | This example might clarify: 355 | 356 | def MyFunction(s1, s2, arg1, arg2): 357 | print "s1=%s, s2=%s, arg1=%s, arg2=%s" % (`s1`, `s2`, `arg1`, `arg2`) 358 | mycb = Callback(MyFunction, 3, 4) 359 | RegisterCallback(mycb) 360 | ... 361 | #someone calls: 362 | cb(1, 2) 363 | # This will cause MyFunction to be invoked as MyFuntion(1,2,3,4), 364 | # the arguments 1 and 2 tacked on to the front, 365 | # coming from the callback invoker, and arguments 3 and 4 366 | # come from the creation of the callback object 367 | # Thus, the following will be printed: 368 | s1=1, s2=2, arg1=3, arg2=4 369 | """ 370 | def __init__(self, callback, *optArgs, **namedArgs): 371 | self.callback = callback 372 | self.optArgs = optArgs 373 | self.namedArgs = namedArgs 374 | 375 | def __call__(self, *extraArgs): 376 | try: 377 | return self.callback(*extraArgs+self.optArgs, **self.namedArgs) 378 | except KeyboardInterrupt: 379 | raise 380 | except: 381 | print("Error in VCallback %s" % self.__repr__()) 382 | raise 383 | 384 | def __repr__(self): 385 | return "" % repr(self.callback) 386 | 387 | class VCallback: 388 | """This class provides a callback object. 389 | It is similar to the Callback (see the doc for this class), 390 | but is intended to be used in a situation where you have 391 | already collected the optional args and the keyword args. 392 | 393 | Here's an example of when to use this class instead of the Callback class: 394 | 395 | def DefineRegisterCallback(cbfn, *optArgs, *namedArgs): 396 | cb = VCallback(cbfn, optArgs namedArgs) 397 | RegisterCallback(cb) 398 | 399 | ... 400 | 401 | DefineRegisterCallback(MyFunction, 1, 2) 402 | """ 403 | def __init__(self, callback, optArgs, namedArgs): 404 | self.callback = callback 405 | self.optArgs = optArgs 406 | self.namedArgs = namedArgs 407 | 408 | def __call__(self, *extraArgs): 409 | try: 410 | return self.callback(*extraArgs+self.optArgs, **self.namedArgs) 411 | except KeyboardInterrupt: 412 | raise 413 | except: 414 | print("Error in VCallback %s" % self.__repr__()) 415 | print(" extraArgs=%s" % repr(extraArgs)) 416 | print(" self.optArgs=%s" % repr(self.optArgs)) 417 | print(" self.namedArgs=%s" % repr(self.namedArgs)) 418 | raise 419 | 420 | def __repr__(self): 421 | return "" % repr(self.callback) 422 | -------------------------------------------------------------------------------- /py_interface/erl_epmd.py: -------------------------------------------------------------------------------- 1 | ### py_interface -- A Python-implementation of an Erlang node 2 | ### 3 | ### $Id$ 4 | ### 5 | ### Copyright (C) 2002 Tomas Abrahamsson 6 | ### 7 | ### Author: Tomas Abrahamsson 8 | ### 9 | ### This file is part of the Py-Interface library 10 | ### 11 | ### This library is free software; you can redistribute it and/or 12 | ### modify it under the terms of the GNU Library General Public 13 | ### License as published by the Free Software Foundation; either 14 | ### version 2 of the License, or (at your option) any later version. 15 | ### 16 | ### This library is distributed in the hope that it will be useful, 17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | ### Library General Public License for more details. 20 | ### 21 | ### You should have received a copy of the GNU Library General Public 22 | ### License along with this library; if not, write to the Free 23 | ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | ### erl_epmd.py -- handle the communication with the epmd 26 | ### (the Erlang portmapper daemon) 27 | 28 | import sys 29 | import types 30 | import string 31 | import socket 32 | import getopt 33 | 34 | from py_interface import erl_common 35 | from py_interface import erl_async_conn 36 | from py_interface import erl_eventhandler 37 | 38 | NODETYPE_NORMAL = 77 39 | NODETYPE_HIDDEN = 72 40 | 41 | M = "erl_epmd" 42 | 43 | class ErlEpmdError(Exception): 44 | def __init__(self, reason): 45 | self.reason = reason 46 | 47 | class ErlEpmd: 48 | """This class provides a connection to the Erlang Portmapper Daemon, EPMD. 49 | """ 50 | def __init__(self, hostName="localhost", portNum=4369): 51 | """Constructor. 52 | 53 | HOST-NAME = string 54 | Default is "localhost" 55 | PORT-NUM = integer 56 | Default is 4369 57 | 58 | Creates an instance for communicating with the EPMD on HOST-NAME 59 | on PORT-NUM. 60 | Use the Connect method to establish the actual connection to the EPMD. 61 | """ 62 | self._hostName = hostName 63 | self._portNum = portNum 64 | self.connection = None 65 | 66 | def SetOwnPortNum(self, ownPortNum): 67 | """Specify the port number for the node 68 | 69 | OWN-PORT-NUM = integer 70 | 71 | Specify the port number for the socket that serves incoming 72 | connections to the node. This is the portnumber to be published 73 | to the EPMD. Other nodes looks up the nodes portnumber in the 74 | EPMD, then establishes connections to that port. 75 | """ 76 | self._ownPortNum = ownPortNum 77 | 78 | def SetOwnNodeName(self, nodeName): 79 | """Specify the node name for the node 80 | 81 | NODE-NAME = string 82 | Must be on the form alivename@hostname 83 | 84 | Sets the nodes name. This is the node name that is published in 85 | the EPMD. 86 | """ 87 | ## XXX Ought to change the scheme: get the host from the node-name 88 | ## instead of from an argument to the constructor? 89 | self._ownNodeName = nodeName 90 | 91 | def Connect(self, connectedCb, connectFailedCb): 92 | """Connect to the EPMD and issue an Alive2 request. 93 | 94 | CONNECTED-CB = 95 | CREATION = integer 96 | CONNECT-FAILED-CB = 97 | RESULT = integer 98 | 99 | Connects to the EPMD specified in the constructor, then publishes 100 | the own node name and port by issuing an Alive2 request. 101 | 102 | The SetOwnPortNum and SetOwnNodeName methods must have been called 103 | prior to calling this method. 104 | """ 105 | self._connectedCb = connectedCb 106 | self._connectFailedCb = connectFailedCb 107 | self._epmdConn = ErlEPMDStdConnection() 108 | if not self._epmdConn.Connect(self._hostName, self._portNum): 109 | raise ErlEpmdError("Connection to EPMD failed") 110 | self.Alive2Req(self._ownPortNum, NODETYPE_HIDDEN, 111 | (5, 5), self._ownNodeName, "", self._Alive2RespCb) 112 | 113 | def Close(self): 114 | """Close the connection to the EPMD.""" 115 | self.AliveCloseReq(self._AliveCloseSink) 116 | 117 | ## Requests 118 | ## 119 | 120 | def AliveReq(self, portNum, nodeName, cb): 121 | """Issue an Alive request. 122 | PORT-NUM = integer 123 | The port number of the socket that serves incoming 124 | connections to the node. 125 | NODE-NAME = string 126 | the name of the node (on the form alive@host) 127 | 128 | This request has no callback. 129 | """ 130 | self._epmdConn.AliveReq(portNum, nodeName, cb) 131 | 132 | def Alive2Req(self, portNum, nodeType, distrVSNRange, nodeName, extra, cb): 133 | """Issue an Alive2 request. 134 | PORT-NUM = integer 135 | The port number of the socket that serves incoming 136 | connections to the node. 137 | NODE-TYPE = integer 138 | DISTR-VSN-RANGE = tuple(LO, HI) 139 | LO = HI = integer 140 | NODE-NAME = string 141 | the name of the node (on the form alive@host) 142 | EXTRA = string 143 | CB = 144 | CREATION = integer 145 | 146 | Calls the callback CB when the answer is available. 147 | See the file distribution_handshake.txt in the erlang distribution 148 | for more info on the NODE-TYPE, DISTR-VSN-RANGE and EXTRA arguments. 149 | """ 150 | self._epmdConn.Alive2Req(portNum, nodeType, distrVSNRange, 151 | nodeName, extra, cb) 152 | 153 | def AliveCloseReq(self, cb): 154 | """Issue an AliveClose request. 155 | CB = 156 | 157 | This effectively closes the socket connection to the EPMD. 158 | """ 159 | self._epmdConn.AliveCloseReq(cb) 160 | 161 | def PortPleaseReq(self, nodeName, callback): 162 | """Issue a PortPlease request. 163 | NODE-NAME = string 164 | CALLBACK = 165 | PORT-NUMBER = integer 166 | Calls the CALLBACK function with argument PORT-NUMBER when 167 | the answer is available. 168 | """ 169 | e = ErlEPMDOneShotConnection(self._hostName, self._portNum) 170 | e.PortPleaseReq(nodeName, callback) 171 | 172 | def PortPlease2Req(self, nodeName, callback): 173 | """Issue a PortPlease2 request. 174 | NODE-NAME = string 175 | CALLBACK = 177 | RESULT = 1 | 0 178 | NODE-TYPE = integer 179 | PROTOCOL = integer 180 | DISTR-VSN-RANGE = tuple(LO, HI) 181 | LO = HI = integer 182 | RNODE-NAME = string 183 | EXTRA = string 184 | Calls the CALLBACK function when the answer is available from the EPMD. 185 | If the RESULT is 0, then the values of the rest of the arguments 186 | are undefined. If the result is 1, then the rest of the arguments 187 | have the values as reported from the EPMD. 188 | 189 | Calls the CALLBACK function with argument PORT-NUMBER when 190 | the answer is available. 191 | """ 192 | e = ErlEPMDOneShotConnection(self._hostName, self._portNum) 193 | e.PortPlease2Req(nodeName, callback) 194 | 195 | def NamesReq(self, callback): 196 | """Issue a Names request 197 | CALLBACK = 275 | PORT-NUMBER = integer 276 | Calls the CALLBACK function with argument PORT-NUMBER when 277 | the answer is available. 278 | """ 279 | msg = self.PackInt1(self._PORT_PLEASE_REQ) + nodeName 280 | unpackcb = erl_common.Callback(self._UnpackPortPleaseResp, callback) 281 | self._SendOneShotReq(msg, unpackcb) 282 | 283 | def PortPlease2Req(self, nodeName, callback): 284 | """Issue a PortPlease2 request. 285 | NODE-NAME = string 286 | CALLBACK = 288 | RESULT = 1 | 0 289 | NODE-TYPE = integer 290 | PROTOCOL = integer 291 | DISTR-VSN-RANGE = tuple(LO, HI) 292 | LO = HI = integer 293 | RNODE-NAME = string 294 | EXTRA = string 295 | Calls the CALLBACK function when the answer is available from the EPMD. 296 | If the RESULT is 0, then the values of the rest of the arguments 297 | are undefined. If the result is 1, then the rest of the arguments 298 | have the values as reported from the EPMD. 299 | 300 | Calls the CALLBACK function with argument PORT-NUMBER when 301 | the answer is available. 302 | """ 303 | nodeName = nodeName.encode("latin1") 304 | msg = self.PackInt1(self._PORT_PLEASE2_REQ) + nodeName 305 | unpackcb = erl_common.Callback(self._UnpackPortPlease2Resp, callback) 306 | self._SendOneShotReq(msg, unpackcb) 307 | 308 | def NamesReq(self, callback): 309 | """Issue a Names request 310 | CALLBACK = 460 | CREATION = integer 461 | 462 | Calls the callback CB when the answer is available. 463 | See the file distribution_handshake.txt in the erlang distribution 464 | for more info on the NODE-TYPE, DISTR-VSN-RANGE and EXTRA arguments. 465 | """ 466 | aliveName = bytes(nodeName.split("@")[0], "latin1") 467 | extra = bytes(extra, "latin1") 468 | msg = (self.PackInt1(self._ALIVE2_REQ) + 469 | self.PackInt2(portNum) + 470 | self.PackInt1(nodeType) + 471 | self.PackInt1(0) + # protocol: 0 = tcp/ip-v4 472 | self.PackInt2(distrVSNRange[0]) + 473 | self.PackInt2(distrVSNRange[1]) + 474 | self.PackInt2(len(aliveName)) + aliveName + 475 | self.PackInt2(len(extra)) + extra) 476 | self._SendReq(msg, cb) 477 | 478 | 479 | def AliveReq(self, portNum, nodeName): 480 | """Issue an Alive request. 481 | PORT-NUM = integer 482 | The port number of the socket that serves incoming 483 | connections to the node. 484 | NODE-NAME = string 485 | the name of the node (on the form alive@host) 486 | 487 | This request has no callback. 488 | """ 489 | nodeName = bytes(nodeName, "latin1") 490 | msg = (self.PackInt1(self._ALIVE_REQ) + 491 | self.PackInt2(portNum) + 492 | nodeName) 493 | self._SendReq(msg, cb) 494 | 495 | def AliveCloseReq(self, cb): 496 | """Issue an AliveClose request. 497 | CB = 498 | 499 | This effectively closes the socket connection to the EPMD. 500 | """ 501 | self.Close() 502 | cb() 503 | 504 | 505 | ## 506 | ## Internal routines 507 | ## 508 | 509 | ## 510 | ## Sending 511 | ## 512 | 513 | def _SendReq(self, req, cb): 514 | if not self._isConnected: 515 | raise ErlEpmdError("not connected to epmd") 516 | self._NewCurrReq(req[0], cb) 517 | msg = self.PackInt2(len(req)) + req 518 | self.Send(msg) 519 | 520 | 521 | def _NewCurrReq(self, reqId, cb): 522 | self._currentRequests.append((reqId, cb)) 523 | 524 | 525 | def _GetCurrReqId(self): 526 | if len(self._currentRequests) == 0: 527 | return None 528 | else: 529 | return self._currentRequests[0][0] 530 | 531 | def _GetCurrReqCb(self): 532 | if len(self._currentRequests) == 0: 533 | return None 534 | else: 535 | return self._currentRequests[0][1] 536 | 537 | 538 | def _CurrReqDone(self): 539 | self._currentRequests = self._currentRequests[1:] 540 | 541 | 542 | ## 543 | ## Handling incoming data 544 | ## 545 | 546 | def _In(self): 547 | is_closed = 0 548 | data = self._connection.recv(100000) 549 | if len(data) == 0: 550 | # closed connection 551 | is_closed = 1 552 | 553 | newInput = self._pendingInput + data 554 | 555 | # split into chunks separated by newline sequence 556 | # call the callback for each of these chunks 557 | 558 | newPendingInput = self._HandleMsgs(newInput) 559 | self._pendingInput = newPendingInput 560 | 561 | if is_closed: 562 | self._OtherEndClosedConnection() 563 | 564 | 565 | def _OtherEndClosedConnection(self): 566 | if self._GetCurrReqId() == self._ALIVE_REQ: # alive_req 567 | self.AliveNotOkResp() 568 | self._CurrReqDone() 569 | else: 570 | self.ConnectionClosed() 571 | ## close our end 572 | self.Close() 573 | 574 | 575 | def _HandleMsgs(self, input): 576 | toBeUnpacked = input 577 | while 1: 578 | (parsedOk, remainingInput) = self._HandleMsg(toBeUnpacked) 579 | if not parsedOk: 580 | return remainingInput 581 | else: 582 | self._CurrReqDone() 583 | toBeUnpacked = remainingInput 584 | 585 | 586 | def _HandleMsg(self, data): 587 | dataLen = len(data) 588 | if dataLen < 3: 589 | return (0, data) 590 | 591 | data0 = data[0] 592 | if data0 == self._ALIVE_OK_RESP and \ 593 | self._GetCurrReqId() == self._ALIVE_REQ: 594 | if dataLen < 3: 595 | return (0, data) 596 | creation = self.ReadInt2(data[1:3]) 597 | cb = self._GetCurrReqCb() 598 | cb(creation) 599 | self._CurrReqDone() 600 | return (1, data[3:]) 601 | elif data0 == self._ALIVE2_RESP and \ 602 | self._GetCurrReqId() == self._ALIVE2_REQ: 603 | if dataLen < 4: 604 | return (0, data) 605 | result = self.ReadInt1(data[1]) 606 | creation = self.ReadInt2(data[2:4]) 607 | cb = self._GetCurrReqCb() 608 | cb(result, creation) 609 | self._CurrReqDone() 610 | return (1, data[4:]) 611 | 612 | currReqTxt = "current request is %s" % repr(self._GetCurrReqId()) 613 | erl_common.DebugHex(M, "unexpected msg, trown away, "+currReqTxt, data) 614 | return (0, "") 615 | -------------------------------------------------------------------------------- /py_interface/erl_eventhandler.py: -------------------------------------------------------------------------------- 1 | ### py_interface -- A Python-implementation of an Erlang node 2 | ### 3 | ### $Id$ 4 | ### 5 | ### Copyright (C) 2002 Tomas Abrahamsson 6 | ### 7 | ### Author: Tomas Abrahamsson 8 | ### 9 | ### This file is part of the Py-Interface library 10 | ### 11 | ### This library is free software; you can redistribute it and/or 12 | ### modify it under the terms of the GNU Library General Public 13 | ### License as published by the Free Software Foundation; either 14 | ### version 2 of the License, or (at your option) any later version. 15 | ### 16 | ### This library is distributed in the hope that it will be useful, 17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | ### Library General Public License for more details. 20 | ### 21 | ### You should have received a copy of the GNU Library General Public 22 | ### License along with this library; if not, write to the Free 23 | ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | ### erl_eventhandler.py -- A general event handler. 26 | ### It provides the possibility to register 27 | ### callbacks to be called at certain events: 28 | ### * when there is data to read on a file descriptor 29 | ### * when it's posssible to write to a file descriptor 30 | ### * at file descriptor exceptions 31 | ### * after timeout 32 | 33 | 34 | 35 | import os 36 | import sys 37 | import string 38 | import select 39 | import time 40 | try: 41 | from tkinter import tkinter 42 | import tkinter 43 | except: pass 44 | 45 | from py_interface import erl_common 46 | 47 | _evhandler = None 48 | 49 | class ErlEventHandlerError (Exception): 50 | def __init__(self, reason): 51 | self.reason = reason 52 | 53 | ### --------------------------------------------------- 54 | ### API 55 | ### 56 | 57 | def GetEventHandler(): 58 | """Retrieve the eventhandler. 59 | No arguments. 60 | Returns: 61 | Throws: nothing 62 | """ 63 | global _evhandler 64 | 65 | if _evhandler == None: 66 | ## This is the first call to the event handler. It has not been 67 | ## created yet, so create it first. 68 | _evhandler = _EventHandler() 69 | return _evhandler 70 | 71 | def SetEventHandlerStateTk(top): 72 | """Sets up the event handler to use Tkinter's mainloop. 73 | 74 | TOP = 75 | 76 | Returns: void 77 | Throws: nothing 78 | 79 | Call this when you want to use the event handler in a program that uses 80 | Tkinter. The argument is expected to be the object that's returned by 81 | a call to Tkinter.Tk(). 82 | """ 83 | evhandler = GetEventHandler() 84 | evhandler._SetStateTk(top) 85 | 86 | def SetEventHandlerStateStandalone(): 87 | """Sets up the event handler to use its own mainloop. This is the default. 88 | 89 | No arguments 90 | Returns: void 91 | Throws: nothing 92 | """ 93 | evhandler = GetEventHandler() 94 | evhandler._SetStateStandAlone() 95 | 96 | ### 97 | ### End of API 98 | ### --------------------------------------------------- 99 | 100 | class EVCallback: 101 | """This class is intended to be used by the _EventHandler class. 102 | It differs from erl_common.VCallback in that it does not 103 | tack on any extra arguments to the front. This is because tkinter 104 | sends some extra arguments to the callback, so the arguments to 105 | callbacks would have been depended on whether the event handler 106 | is in tkinter-state or not. 107 | """ 108 | def __init__(self, callback, optArgs, namedArgs): 109 | self.callback = callback 110 | self.optArgs = optArgs 111 | self.namedArgs = namedArgs 112 | 113 | def __call__(self, *extraArgs): 114 | try: 115 | return self.callback(*self.optArgs, **self.namedArgs) 116 | except KeyboardInterrupt: 117 | raise 118 | except: 119 | print("Error in EVCallback %s" % self.__repr__()) 120 | raise 121 | 122 | def __repr__(self): 123 | return "" % repr(self.callback) 124 | 125 | _tk = None 126 | def _GetTk(): 127 | global _tk 128 | if _tk == None: 129 | _tk = tkinter.Frame().tk 130 | return _tk 131 | 132 | 133 | _nextTimerId = 0 134 | def GetNewTimerId(): 135 | global _nextTimerId 136 | 137 | idToReturn = _nextTimerId 138 | _nextTimerId = _nextTimerId + 1 139 | return idToReturn 140 | 141 | class _TimerEvent: 142 | def __init__(self, timeLeft, cb): 143 | self.addedWhen = time.time() 144 | self.id = GetNewTimerId() 145 | self.timeOutTime = self.addedWhen + timeLeft 146 | self.cb = cb 147 | 148 | def __lt__(self, other): 149 | return self.timeOutTime < other.timeOutTime 150 | 151 | def __eq__(self, other): 152 | return self.timeOutTime == other.timeOutTime 153 | 154 | class _EventHandler: 155 | STATE_STANDALONE = 1 156 | STATE_TK = 2 157 | 158 | READ = 1 159 | WRITE = 2 160 | EXCEPT = 4 161 | 162 | def __init__(self): 163 | # mappings of connection --> callback 164 | self.readEvents = {} 165 | self.writeEvents = {} 166 | self.exceptEvents = {} 167 | 168 | # A sorted list of timers: the timer with least time to run is first 169 | self.timerEvents = [] 170 | 171 | # State: STATE_STANDALONE -- Tk is not involved 172 | # STATE_TK -- use the eventhandler in Tkinter 173 | self.state = self.STATE_STANDALONE 174 | self.tkTopLevel = None 175 | 176 | def _SetStateTk(self, topLevel): 177 | """State that we are using eventhandler in Tkinter. 178 | Note: When using the Tkinter eventhandler, you cannot delete timer-events.""" 179 | self.state = self.STATE_TK 180 | self.tkTopLevel = topLevel 181 | 182 | def SetStateStandAlone(self): 183 | """State that we are implementing our own eventhandler.""" 184 | self.state = self.STATE_STANDALONE 185 | 186 | 187 | def PushReadEvent(self, connection, cbfunction, *optArgs, **namedArgs): 188 | """Register a callback to be called when there's data to read 189 | on a connection. Or really, push the the callback onto the 190 | stack of read-callbacks for the connection. Only the first callback 191 | on the stack is called. 192 | 193 | CONNECTION = 194 | CB-FUNCTION = 195 | OPT-ARG ... = 196 | NAMED-ARG ... = 197 | 198 | Returns: void 199 | Throws: nothing 200 | """ 201 | cb = EVCallback(cbfunction, optArgs, namedArgs) 202 | if connection in self.readEvents: 203 | handlers = self.readEvents[connection] 204 | else: 205 | handlers = [] 206 | newHandlers = self._PushHandler(cb, handlers) 207 | self.readEvents[connection] = newHandlers 208 | if self.state == self.STATE_TK: 209 | self._TkPushFileHandler(self.READ, connection) 210 | 211 | 212 | def PopReadEvent(self, connection): 213 | """Unregister a read callback for a connection. 214 | Or really, pop it from the stack of callbacks. 215 | 216 | CONNECTION = 217 | 218 | Returns: void 219 | Throws: nothing 220 | """ 221 | handlers = self.readEvents[connection] 222 | newHandlers = self._PopHandler(handlers) 223 | if len(newHandlers) == 0: 224 | del self.readEvents[connection] 225 | else: 226 | self.readEvents[connection] = newHandlers 227 | if self.state == self.STATE_TK: 228 | self._TkPopFileHandler(self.READ, connection) 229 | 230 | 231 | def PushWriteEvent(self, connection, cbfunction, *optArgs, **namedArgs): 232 | """Register a callback to be called when there's data to write 233 | to a connection. Or really, push the the callback onto the 234 | stack of write-callbacks for the connection. Only the first callback 235 | on the stack is called. 236 | 237 | CONNECTION = 238 | CB-FUNCTION = 239 | OPT-ARG ... = 240 | NAMED-ARG ... = 241 | 242 | Returns: void 243 | Throws: nothing 244 | """ 245 | cb = EVCallback(cbfunction, optArgs, namedArgs) 246 | if connection in self.writeEvents: 247 | handlers = self.writeEvents[connection] 248 | else: 249 | handlers = [] 250 | newHandlers = self._PushHandler(cb, handlers) 251 | self.writeEvents[connection] = newHandlers 252 | if self.state == self.STATE_TK: 253 | self._TkPushFileHandler(self.WRITE, connection) 254 | 255 | def PopWriteEvent(self, connection): 256 | """Unregister a write callback for a connection. 257 | Or really, pop it from the stack of callbacks. 258 | 259 | CONNECTION = 260 | 261 | Returns: void 262 | Throws: nothing 263 | """ 264 | handlers = self.writeEvents[connection] 265 | newHandlers = self._PopHandler(handlers) 266 | if len(newHandlers) == 0: 267 | del self.writeEvents[connection] 268 | else: 269 | self.writeEvents[connection] = newHandlers 270 | if self.state == self.STATE_TK: 271 | self._TkPopFileHandler(self.WRITE, connection) 272 | 273 | 274 | def PushExceptEvent(self, connection, cbfunction, *optArgs, **namedArgs): 275 | """Register a callback to be called when there's an exception 276 | on a connection. Or really, push the the callback onto the 277 | stack of exception-callbacks for the connection. 278 | Only the first callback on the stack is called. 279 | 280 | CONNECTION = 281 | CB-FUNCTION = 282 | OPT-ARG ... = 283 | NAMED-ARG ... = 284 | 285 | Returns: void 286 | Throws: nothing 287 | """ 288 | cb = EVCallback(cbfunction, optArgs, namedArgs) 289 | if connection in self.exceptEvents: 290 | handlers = self.exceptEvents[connection] 291 | else: 292 | handlers = [] 293 | newHandlers = self._PushHandler(cb, handlers) 294 | self.exceptEvents[connection] = newHandlers 295 | if self.state == self.STATE_TK: 296 | self._TkPushFileHandler(self.EXCEPT, connection) 297 | 298 | def PopExceptEvent(self, connection): 299 | """Unregister an exception callback for a connection. 300 | Or really, pop it from the stack of callbacks. 301 | 302 | CONNECTION = 303 | 304 | Returns: void 305 | Throws: nothing 306 | """ 307 | handlers = self.exceptEvents[connection] 308 | newHandlers = self._PopHandler(handlers) 309 | if len(newHandlers) == 0: 310 | del self.exceptEvents[connection] 311 | else: 312 | self.exceptEvents[connection] = newHandlers 313 | if self.state == self.STATE_TK: 314 | self._TkPopFileHandler(self.EXCEPT, connection) 315 | 316 | 317 | def AddTimerEvent(self, timeLeft, cbfunction, *optArgs, **namedArgs): 318 | """Register a callback to be called after a certain amount of time. 319 | 320 | TIME-LEFT = integer | float 321 | Timeout (in seconds) for the timer 322 | CB-FUNCTION = 323 | OPT-ARG ... = 324 | NAMED-ARG ... = 325 | 326 | Returns: timer-id 327 | Throws: nothing 328 | """ 329 | cb = EVCallback(cbfunction, optArgs, namedArgs) 330 | if self.state == self.STATE_STANDALONE: 331 | newTimerEvent = _TimerEvent(timeLeft, cb) 332 | self.timerEvents.append(newTimerEvent) 333 | self.timerEvents.sort() 334 | return newTimerEvent.id 335 | elif self.state == self.STATE_TK: 336 | return _GetTk().createtimerhandler(int(round(timeLeft * 1000)), cb) 337 | 338 | def DelTimerEvent(self, id): 339 | """Unregister a timer callback. 340 | 341 | TIMER-ID = timer-id 342 | 343 | Returns: nothing 344 | Throws: "Cannot delete timer events when using tk" 345 | 346 | Note that it is not possible to unregister timer events when using Tk. 347 | """ 348 | if self.state == self.STATE_TK: 349 | raise ErlEventHandlerError("Cannot delete timers when using tk") 350 | 351 | indexForId = None 352 | it = zip(range(len(self.timerEvents)), self.timerEvents) 353 | for i, ev in it: 354 | if ev.id == id: 355 | indexForId = i 356 | break 357 | if indexForId != None: 358 | del self.timerEvents[indexForId] 359 | 360 | def Loop(self): 361 | """Start the event handler. 362 | 363 | No arguments: 364 | Returns: void 365 | Throws: nothing 366 | """ 367 | if self.state == self.STATE_TK: 368 | self.__LoopTk() 369 | elif self.state == self.STATE_STANDALONE: 370 | self.__LoopStandalone() 371 | 372 | 373 | def __LoopTk(self): 374 | self.tkTopLevel.mainloop() 375 | 376 | def __LoopStandalone(self): 377 | self.continueLooping = 1 378 | while self.continueLooping: 379 | rList = list(self.readEvents.keys()) 380 | wList = list(self.writeEvents.keys()) 381 | eList = list(self.exceptEvents.keys()) 382 | 383 | timeout = None 384 | if len(self.timerEvents) > 0: 385 | firstTimerEv = self.timerEvents[0] 386 | now = time.time() 387 | timeout = firstTimerEv.timeOutTime - now 388 | 389 | if timeout == None: 390 | # No timer to wait for 391 | try: 392 | reads, writes, excepts = select.select(rList, wList, eList) 393 | except select.error as info: 394 | (errno, errText) = info 395 | if errno == 4: 396 | # 'Interrupted system call' 397 | # ignore this one. 398 | # loop again in the while loop 399 | continue 400 | else: 401 | # Other error: serious. Raise again. 402 | raise 403 | 404 | elif timeout < 0: 405 | # Signal timeout 406 | (reads, writes, excepts) = ([], [], []) 407 | else: 408 | # Select and wait for timer 409 | try: 410 | reads, writes, excepts = select.select(rList, wList, eList, 411 | timeout) 412 | except select.error as info: 413 | (errno, errText) = info 414 | if errno == 4: 415 | # 'Interrupted system call' 416 | # ignore this one. 417 | # loop again in the while loop 418 | continue 419 | else: 420 | # Other error: serious. Raise again. 421 | raise 422 | 423 | # Check for handles that are clear for reading 424 | for readable in reads: 425 | cb = self.readEvents[readable][0] 426 | cb() 427 | 428 | # Check for handles that are clear for writing 429 | for writable in writes: 430 | cb = self.writeEvents[writable][0] 431 | cb() 432 | 433 | # Check for handles that are in exception situation 434 | for exceptable in excepts: 435 | cb = self.exceptEvents[exceptable][0] 436 | cb() 437 | 438 | # Check for timers that have timed out 439 | while 1: 440 | expiredTimers = list(filter(lambda ev, now=time.time(): 441 | ev.timeOutTime <= now, 442 | self.timerEvents)) 443 | if len(expiredTimers) == 0: 444 | break # skip the while loop 445 | 446 | for expiredTimer in expiredTimers: 447 | expiredTimer.cb() 448 | self.DelTimerEvent(expiredTimer.id) 449 | 450 | 451 | def StopLooping(self): 452 | """Start the event handler. 453 | 454 | No arguments: 455 | Returns: void 456 | Throws: nothing 457 | 458 | This can use in e.g. callbacks to stop the event handler loop.""" 459 | self.continueLooping = 0 460 | 461 | def _PushHandler(self, cb, handlers): 462 | return [cb] + handlers 463 | 464 | def _PopHandler(self, handlers): 465 | return handlers[1:] 466 | 467 | def _TkPushFileHandler(self, eventType, connection): 468 | fileNum = connection.fileno() 469 | if eventType == self.READ: 470 | cb = self.readEvents[connection][0] 471 | _GetTk().createfilehandler(fileNum, tkinter.READABLE, cb) 472 | elif eventType == self.WRITE: 473 | cb = self.writeEvents[connection][0] 474 | _GetTk().createfilehandler(fileNum, tkinter.WRITABLE, cb) 475 | elif eventType == self.EXCEPT: 476 | cb = self.exceptEvents[connection][0] 477 | _GetTk().createfilehandler(fileNum, tkinter.EXCEPTION,cb) 478 | 479 | def _TkPopFileHandler(self, eventType, connection): 480 | fileNum = connection.fileno() 481 | ## In tkinter, all handlers (readable as well as writable/exception) 482 | ## are deleted when we delete a handler. 483 | ## The net result is that the code is all the same no matter 484 | ## what type of handler we delete. 485 | _GetTk().deletefilehandler(fileNum) 486 | if connection in self.readEvents: 487 | cb = self.readEvents[connection][0] 488 | _GetTk().createfilehandler(fileNum, tkinter.READABLE, cb) 489 | if connection in self.writeEvents: 490 | cb = self.writeEvents[connection][0] 491 | _GetTk().createfilehandler(fileNum, tkinter.WRITABLE, cb) 492 | if connection in self.exceptEvents: 493 | cb = self.exceptEvents[connection][0] 494 | _GetTk().createfilehandler(fileNum, tkinter.EXCEPTION,cb) 495 | -------------------------------------------------------------------------------- /py_interface/erl_node_conn.py: -------------------------------------------------------------------------------- 1 | ### py_interface -- A Python-implementation of an Erlang node 2 | ### 3 | ### $Id$ 4 | ### 5 | ### Copyright (C) 2002 Tomas Abrahamsson 6 | ### 7 | ### Author: Tomas Abrahamsson 8 | ### 9 | ### This file is part of the Py-Interface library 10 | ### 11 | ### This library is free software; you can redistribute it and/or 12 | ### modify it under the terms of the GNU Library General Public 13 | ### License as published by the Free Software Foundation; either 14 | ### version 2 of the License, or (at your option) any later version. 15 | ### 16 | ### This library is distributed in the hope that it will be useful, 17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | ### Library General Public License for more details. 20 | ### 21 | ### You should have received a copy of the GNU Library General Public 22 | ### License along with this library; if not, write to the Free 23 | ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | ### erl_node_conn.py -- Handle inter-node communication 26 | 27 | import sys 28 | import time 29 | import types 30 | import string 31 | import socket 32 | import random 33 | try: 34 | from hashlib import md5 35 | except ImportError: 36 | # Pre-Python2.5 library for md5 37 | from md5 import new as md5 38 | 39 | 40 | from py_interface import erl_opts 41 | from py_interface import erl_term 42 | from py_interface import erl_common 43 | from py_interface import erl_async_conn 44 | from py_interface import erl_eventhandler 45 | 46 | M = "erl_node_conn" 47 | 48 | def CheckDigest(digest, challenge, cookie): 49 | """Checks that a digest is correct. 50 | DIGEST = bytes 51 | CHALLENGE = integer 52 | COOKIE = string 53 | 54 | Returns: 1 | 0 55 | Throws: nothing 56 | """ 57 | expectedDigest = GenDigest(challenge, cookie) 58 | return expectedDigest == digest 59 | 60 | def GenDigest(challenge, cookie): 61 | """Generates a digest from a CHALLENGE and a COOKIE. 62 | CHALLENGE = integer 63 | COOKIE = string 64 | 65 | Returns: string 66 | Throws: nothing 67 | """ 68 | challengeBytes = bytes(str(challenge), "ascii") 69 | return md5(bytes(cookie, "ascii") + challengeBytes).digest() 70 | 71 | def GenChallenge(): 72 | """Generates a challenge. 73 | No arguments. 74 | Returns: integer 75 | Throws: nothing 76 | """ 77 | return int(random.random() * 0x7fffffff) 78 | 79 | 80 | class Ticker: 81 | """This class is used for keeping track of the net-ticks: 82 | * when a remote node has been silent for too long 83 | * when it's time to send a tick so the other part won't think 84 | we've been silent for to long (to `tick'). 85 | """ 86 | 87 | def __init__(self, netTickTime, timeToTickCb, noResponseCb): 88 | """Constructor 89 | NET-TICK-TIME = integer 90 | the net-tick-time (in seconds). 91 | TIME-TO-TICK-CB = 92 | callback to call when it's time to tick. 93 | NO-RESPONSE-CB = 94 | callback to call when the other end has been 95 | silent for too long. 96 | 97 | Throws: nothing 98 | """ 99 | self._netTickTime = netTickTime 100 | self._evhandler = erl_eventhandler.GetEventHandler() 101 | self._InitStartResponseTimer(netTickTime, noResponseCb) 102 | self._InitStartTickTimer(netTickTime, timeToTickCb) 103 | 104 | def _InitStartResponseTimer(self, netTickTime, noResponseCb): 105 | self._noResponseCb = noResponseCb 106 | self._noResponseTimeout = netTickTime * 1.25 107 | self._responseCheckTimeout = netTickTime * 0.25 108 | self._responseDoCheck = 1 109 | self._timeForLastResponse = time.time() 110 | self._StartResponseTimer() 111 | 112 | def _StartResponseTimer(self): 113 | if self._responseDoCheck: 114 | timeout = self._responseCheckTimeout 115 | cb = self._CheckResponse 116 | timerId = self._evhandler.AddTimerEvent(timeout, cb) 117 | self._checkResponseTimerId = timerId 118 | 119 | def _StopResponseTimer(self): 120 | self._responseDoCheck = 0 121 | 122 | def GotResonse(self): 123 | """To be called whenever data has been received from the other end. 124 | No arguments. 125 | Returns: void 126 | Throws: nothing 127 | """ 128 | self._timeForLastResponse = time.time() 129 | 130 | def _CheckResponse(self): 131 | if self._responseDoCheck: 132 | now = time.time() 133 | if now > self._timeForLastResponse + self._noResponseTimeout: 134 | self._responseDoCheck = 0 135 | self._noResponseCb() 136 | else: 137 | self._StartResponseTimer() 138 | 139 | def _InitStartTickTimer(self, netTickTime, timeToTickCb): 140 | self._timeToTickCb = timeToTickCb 141 | self._tickTimeout = netTickTime * 0.25 142 | self._tickCheckTimeout = netTickTime * 0.125 143 | self._tickDoCheck = 1 144 | self._timeForLastTick = time.time() 145 | self._StartTickTimer() 146 | 147 | def _StartTickTimer(self): 148 | if self._tickDoCheck: 149 | timeout = self._tickCheckTimeout 150 | cb = self._Tick 151 | timerId = self._evhandler.AddTimerEvent(timeout, cb) 152 | self._tickTimerId = timerId 153 | 154 | def _StopTickTimer(self): 155 | self._tickDoCheck = 0 156 | 157 | def RestartTick(self): 158 | """To be called whenever something has been sent to the other end. 159 | No arguments. 160 | Returns: void 161 | Throws: nothing 162 | """ 163 | self._timeForLastTick = time.time() 164 | 165 | def _Tick(self): 166 | if self._tickDoCheck: 167 | self._StartTickTimer() 168 | now = time.time() 169 | if now > self._timeForLastTick + self._tickTimeout: 170 | self._timeToTickCb() 171 | self._timeForLastTick = time.time() 172 | 173 | def Stop(self): 174 | """Stop the timers. 175 | No arguments. 176 | Returns: void 177 | Throws: nothing 178 | """ 179 | self._StopResponseTimer() 180 | self._StopTickTimer() 181 | 182 | class ErlNodeOutConnection(erl_async_conn.ErlAsyncClientConnection): 183 | """This class handles a connection _to_ another node, 184 | initiated by this node. 185 | 186 | Inheritance: erl_async_conn.ErlAsyncClientConnection 187 | 188 | This is intended to be used by the erl_node.ErlNode class. 189 | """ 190 | _STATE_DISCONNECTED = -1 191 | _STATE_HANDSHAKE_RECV_STATUS = 2 192 | _STATE_HANDSHAKE_RECV_CHALLENGE = 4 193 | _STATE_HANDSHAKE_RECV_CHALLENGE_ACK = 6 194 | _STATE_CONNECTED = 7 195 | 196 | def __init__(self, nodeName, opts): 197 | """Constructor. 198 | NODE-NAME = string 199 | OPTS = 200 | 201 | Throws: nothing 202 | """ 203 | erl_async_conn.ErlAsyncClientConnection.__init__(self) 204 | self._recvdata = b"" 205 | self._hostName = None 206 | self._portNum = None 207 | self._nodeName = nodeName 208 | self._opts = opts 209 | self._peerName = None 210 | self._peerFlags = 0xffffFFFF 211 | self._state = self._STATE_DISCONNECTED 212 | # 2 bytes for the packet length during the handshake, then 4 bytes 213 | self._packetLenSize = 2 214 | # These are started once the connection is up 215 | self._tickTimers = None 216 | 217 | def InitiateConnection(self, hostName, portNum, 218 | connectOkCb, connectFailedCb, connectionBrokenCb, 219 | passThroughMsgCb): 220 | """Initiates a connection to another erlang-node. 221 | HOST-NAME = string 222 | The node to connect to. 223 | PORT-NUM = integer 224 | The port on that node. Use the EPMD to find 225 | the port number, given a node name. 226 | CONNECT-OK-CB = 227 | To be called when a connection has been 228 | successfully established. 229 | CONNECT-FAILED-CB = 230 | CONNECTION = ErlNodeOutConnection 231 | PEER-NAME = string (the node name for the peer) 232 | To be called when a connection establishment 233 | failed. 234 | CONNECTION-BROKEN-CB = 235 | CONNECTION = ErlNodeOutConnection 236 | PEER-NAME = string (the node name for the peer) 237 | To be called when an established connection 238 | has been broken. 239 | 240 | Returns: void 241 | Throws: <> 242 | """ 243 | self._hostName = hostName 244 | self._portNum = portNum 245 | self._connectOkCb = connectOkCb 246 | self._connectFailedCb = connectFailedCb 247 | self._connectionBrokenCb = connectionBrokenCb 248 | self._passThroughMsgCb = passThroughMsgCb 249 | self._peerName = "(unknown)@%s" % hostName 250 | if self.Connect(hostName, portNum): 251 | self._SendName() 252 | self._state = self._STATE_HANDSHAKE_RECV_STATUS 253 | else: 254 | return 0 255 | 256 | def GetPeerNodeName(self): 257 | """Retrieves the node name for the peer. 258 | No arguments. 259 | Returns: string | "" 260 | Throws: nothing 261 | """ 262 | return self._peerName 263 | 264 | def SendMsg(self, ctrlMsg, msg=None): 265 | """Sends a message to the other end. 266 | CTRL-MSG = term 267 | MSG (optional) = None | term 268 | 269 | Returns: void 270 | Throws: nothing 271 | 272 | For information on the CTRL-MSG and the MSG, please refer to the file 273 | erl_ext_dist.txt in the Erlang distribution. 274 | """ 275 | if msg == None: 276 | packet = b"p" + erl_term.TermToBinary(ctrlMsg) 277 | else: 278 | packet = b"p" + (erl_term.TermToBinary(ctrlMsg) + \ 279 | erl_term.TermToBinary(msg, self._peerFlags)) 280 | self._SendPacket(packet) 281 | 282 | 283 | ## 284 | ## Internal routines 285 | ## 286 | 287 | def _In(self): 288 | """Callback routine, which is called when data is available 289 | on the connection.""" 290 | connection = self.GetConnection() 291 | newData = connection.recv(100000) 292 | if len(newData) == 0: 293 | self.Close() 294 | if self._state != self._STATE_CONNECTED: 295 | self._state = self._STATE_DISCONNECTED 296 | self._connectFailedCb() 297 | else: 298 | self._state = self._STATE_DISCONNECTED 299 | if self._tickTimers != None: 300 | self._tickTimers.Stop() 301 | self._connectionBrokenCb() 302 | return 303 | 304 | self._recvdata = self._recvdata + newData 305 | remainingUnhandledData = self._HandleData(self._recvdata) 306 | self._recvdata = remainingUnhandledData 307 | 308 | def _HandleData(self, data): 309 | remainingInput = data 310 | while True: 311 | if len(remainingInput) < self._packetLenSize: 312 | return remainingInput 313 | 314 | if self._packetLenSize == 2: 315 | packetLen = self.ReadInt2(remainingInput[0:2]) 316 | packetOffset = 2 317 | else: 318 | packetLen = self.ReadInt4(remainingInput[0:4]) 319 | packetOffset = 4 320 | 321 | if len(remainingInput) < self._packetLenSize + packetLen: 322 | return remainingInput 323 | 324 | packetData = remainingInput[packetOffset:packetOffset+packetLen] 325 | self._HandlePacket(packetData) 326 | remainingInput = remainingInput[packetOffset+packetLen:] 327 | 328 | 329 | def _HandlePacket(self, data): 330 | if self._state == self._STATE_HANDSHAKE_RECV_STATUS: 331 | # First check that the correct message came in 332 | if chr(data[0]) != "s": 333 | erl_common.DebugHex(M, "handshake:recv_status: got", data) 334 | self.Close() 335 | self._state = self._STATE_DISCONNECTED 336 | self._connectFailedCb() 337 | status = data[1:] 338 | if status == b"ok" or status == b"ok_simultaneous": 339 | self._state = self._STATE_HANDSHAKE_RECV_CHALLENGE 340 | elif status == b"nok" or status == b"not_allowed": 341 | self.Close() 342 | self._state = self._STATE_DISCONNECTED 343 | self._connectFailedCb() 344 | elif status == b"alive": 345 | self._SendStatusAliveTrue() 346 | self._state = self._STATE_HANDSHAKE_RECV_CHALLENGE 347 | else: 348 | erl_common.DebugHex(M, "handshake:recv_status", data) 349 | elif self._state == self._STATE_HANDSHAKE_RECV_CHALLENGE: 350 | # First check that the correct message came in 351 | if chr(data[0]) != "n": 352 | erl_common.DebugHex(M, "handshake:recv_cha", data) 353 | self.Close() 354 | self._state = self._STATE_DISCONNECTED 355 | self._connectFailedCb() 356 | self._peerVersion = self.ReadInt2(data[1:3]) 357 | self._peerFlags = self.ReadInt4(data[3:7]) 358 | challenge = self.ReadInt4(data[7:11]) 359 | self._peerName = data[11:].decode("latin1") 360 | self._SendChallengeReply(challenge) 361 | self._state = self._STATE_HANDSHAKE_RECV_CHALLENGE_ACK 362 | elif self._state == self._STATE_HANDSHAKE_RECV_CHALLENGE_ACK: 363 | # First check that the correct message came in 364 | if chr(data[0]) != "a": 365 | erl_common.DebugHex(M, "handshake:recv_cha_ack", data) 366 | self.Close() 367 | self._state = self._STATE_DISCONNECTED 368 | self._connectFailedCb() 369 | digest = data[1:] 370 | ownCookie = self._opts.GetCookie() 371 | if CheckDigest(digest, self._challengeToPeer, ownCookie): 372 | self._packetLenSize = 4 373 | self._state = self._STATE_CONNECTED 374 | t = self._opts.GetNetTickTime() 375 | self._tickTimers = Ticker(t, self._Tick, self._NoResponse) 376 | self._connectOkCb() 377 | else: 378 | erl_common.Debug(M, 379 | "Connection attempt to disallowed node %s" % 380 | self._peerName) 381 | self.Close() 382 | self._state = self._STATE_DISCONNECTED 383 | self._connectFailedCb() 384 | elif self._state == self._STATE_CONNECTED: 385 | self._tickTimers.GotResonse() 386 | if len(data) == 0: 387 | # A tick 388 | return 389 | 390 | msgType = chr(data[0]) 391 | if msgType == "p": 392 | terms = erl_term.BinariesToTerms(data[1:]) 393 | if len(terms) == 2: 394 | controlMsg = terms[0] 395 | msg = terms[1] 396 | self._passThroughMsgCb(self, self.GetPeerNodeName(), 397 | controlMsg, msg) 398 | elif len(terms) == 1: 399 | controlMsg = terms[0] 400 | self._passThroughMsgCb(self, self.GetPeerNodeName(), 401 | controlMsg, msg) 402 | else: 403 | debugTxt = "PassThrough-msg: terms=%s" % repr(terms) 404 | erl_common.DebugHex(M, debugTxt, data) 405 | else: 406 | erl_common.DebugHex(M, "msgType=%c" % msgType, data) 407 | else: 408 | erl_common.DebugHex(M, "state=%d" % self._state, data) 409 | 410 | 411 | def _Tick(self): 412 | """This callback is called by the Ticker class instance 413 | when it is time to send a tick to the other end, to indicate that 414 | we are still alive. 415 | """ 416 | self._SendPacket(b"") 417 | 418 | def _NoResponse(self): 419 | """This callback is called by the Ticker class instance 420 | when nothing has been received from the other end for too long. 421 | """ 422 | erl_common.Debug(M, "InConnection: Connection broken") 423 | self._state = self._STATE_DISCONNECTED 424 | self._tickTimers.Stop() 425 | self._connectionBrokenCb() 426 | 427 | 428 | def _SendName(self): 429 | packet = b"n" + \ 430 | self.PackInt2(self._opts.GetDistrVersion()) + \ 431 | self.PackInt4(self._opts.GetDistrFlags()) + \ 432 | bytes(self._nodeName, "latin1") 433 | self._SendHandshakeMsg(packet) 434 | 435 | def _SendStatusAliveTrue(self): 436 | self._SendHandshakeMsg("true") 437 | 438 | def _SendChallengeReply(self, challenge): 439 | digest = GenDigest(challenge, self._opts.GetCookie()) 440 | challengeToPeer = GenChallenge() 441 | self._challengeToPeer = challengeToPeer 442 | packet = b"r" + self.PackInt4(challengeToPeer) + digest 443 | self._SendHandshakeMsg(packet) 444 | 445 | def _SendHandshakeMsg(self, packet): 446 | msg = self.PackInt2(len(packet)) + packet 447 | erl_common.Debug(M, "Sending handshake") 448 | self.Send(msg) 449 | 450 | def _SendPacket(self, packet): 451 | msg = self.PackInt4(len(packet)) + packet 452 | erl_common.Debug(M, "Sending msg") 453 | self._tickTimers.RestartTick() 454 | self.Send(msg) 455 | 456 | 457 | class ErlNodeServerSocket(erl_async_conn.ErlAsyncServer): 458 | """This class opens a socket and for incoming connections from other 459 | Erlang nodes. When a remote node connects, an new instance of 460 | the ErlNodeInConnection is created for handling the new connection. 461 | 462 | This class is indended to be used by the erl_node.ErlNode. 463 | """ 464 | def __init__(self, nodeName, opts): 465 | """Constructor 466 | NODE-NAME = string 467 | The name of this node 468 | OPTS = 469 | 470 | Throws: nothing 471 | """ 472 | erl_async_conn.ErlAsyncServer.__init__(self) 473 | self._nodeName = nodeName 474 | self._opts = opts 475 | self._passThroughMsgCb = self._Sink 476 | self._nodeUpCb = self._Sink 477 | self._nodeDownCb = self._Sink 478 | 479 | def Start(self, nodeUpCb, nodeDownCb, passThroughMsgCb): 480 | """Setup and start to listen for incoming connections. 481 | 482 | NODE-UP-CB = 483 | Callback to call when a new connection has been 484 | established. 485 | NODE-DOWN-CB = 486 | Callback to call when an established connection 487 | has been broken. 488 | PASS-THROUGH-MSG-CB = 490 | Callback to call for pass-through messages. 491 | Currently, all messages incoming messages are 492 | of this type. 493 | (Sub)Types: 494 | 495 | CONNECTION = 496 | The instance of the class that handles the connection 497 | PEER-NAME = string 498 | The node name for the peer node 499 | CTRL-MSG = term 500 | MSG = term 501 | For information on CTRL-MSG and MSG, see the 502 | file erl_ext_dist.txt, which is included in the 503 | Erlang distribution. 504 | 505 | Returns: void 506 | Throws: <> 507 | """ 508 | self._nodeUpCb = nodeUpCb 509 | self._nodeDownCb = nodeDownCb 510 | self._passThroughMsgCb = passThroughMsgCb 511 | return erl_async_conn.ErlAsyncServer.Start(self) 512 | 513 | def _NewConnection(self, s, remoteAddr): 514 | erl_common.Debug(M, "new connection from %s" % repr(remoteAddr)) 515 | inConn = ErlNodeInConnection(s, 516 | self._nodeName, self._opts, 517 | self._nodeUpCb, self._nodeDownCb, 518 | self._passThroughMsgCb) 519 | 520 | def _Sink(self, *a, **kw): 521 | pass 522 | 523 | class ErlNodeInConnection(erl_async_conn.ErlAsyncPeerConnection): 524 | """This class handles incoming connections from other Erlang nodes. 525 | 526 | This class is indended to be used by the ErlNodeSocketServer 527 | (and thus indirectly by the erl_node.ErlNode). 528 | """ 529 | 530 | ## XXX TODO: This node duplicates too much functionality 531 | ## from ErlNodeOutConnection, still there are differences. 532 | ## 533 | ## The differences are in the setting up of the connection; 534 | ## during the handshake sequence, the connecting side 535 | ## (ErlNodeOutConnection) acts the client, while the connected 536 | ## side (ErlNodeInConnection) acts as server. 537 | ## 538 | ## One idea is to maybe separate the state-machine 539 | ## into its own class. 540 | ## 541 | ## Need to think about this one... 542 | 543 | _STATE_DISCONNECTED = -1 544 | _STATE_HANDSHAKE_RECV_NAME = 1 545 | _STATE_HANDSHAKE_RECV_STATUS = 3 546 | _STATE_HANDSHAKE_RECV_CHALLENGE_REPLY = 5 547 | _STATE_CONNECTED = 7 548 | 549 | def __init__(self, sock, nodeName, opts, 550 | newConnectionUpCb, connectionBrokenCb, 551 | passThroughMsgCb): 552 | """Constructor. 553 | SOCK = 554 | The socket for the incoming connection. 555 | NODE-NAME = string 556 | The node name of the node to which this 557 | connection belongs. 558 | OPTS = 559 | Options for the node 560 | NEW-CONNECTION-UP-CB = 561 | Callback to call when a new connection has been 562 | established. 563 | CONNECTION-BROKEN-CB = 564 | Callback to call when an established connection 565 | has been broken. 566 | PASS-THROUGH-MSG-CB = 568 | Callback to call for pass-through messages. 569 | Currently, all messages incoming messages are 570 | of this type. 571 | """ 572 | erl_async_conn.ErlAsyncPeerConnection.__init__(self, sock) 573 | self._recvdata = b"" 574 | self._hostName = None 575 | self._portNum = None 576 | self._nodeName = nodeName 577 | self._opts = opts 578 | self._newConnectionUpCb = newConnectionUpCb 579 | self._connectionBrokenCb = connectionBrokenCb 580 | self._passThroughMsgCb = passThroughMsgCb 581 | self._state = self._STATE_HANDSHAKE_RECV_NAME 582 | self._peerName = nodeName 583 | self._peerFlags = 0xffffFFFF 584 | # 2 bytes for the packet length during the handshake, then 4 bytes 585 | self._packetLenSize = 2 586 | # These are started once the connection is up 587 | self._tickTimers = None 588 | 589 | def GetPeerNodeName(self): 590 | """Retrieves the node name for the peer. 591 | No arguments. 592 | Returns: string | "" 593 | Throws: nothing 594 | """ 595 | return self._peerName 596 | 597 | def SendMsg(self, ctrlMsg, msg=None): 598 | """Sends a message to the other end. 599 | CTRL-MSG = term 600 | MSG (optional) = None | term 601 | 602 | Returns: void 603 | Throws: nothing 604 | 605 | For information on the CTRL-MSG and the MSG, please refer to the file 606 | erl_ext_dist.txt in the Erlang distribution. 607 | """ 608 | if msg == None: 609 | packet = b"p" + erl_term.TermToBinary(ctrlMsg) 610 | else: 611 | packet = b"p" + (erl_term.TermToBinary(ctrlMsg) + \ 612 | erl_term.TermToBinary(msg, self._peerFlags)) 613 | self._SendPacket(packet) 614 | 615 | 616 | ## 617 | ## Internal routines 618 | ## 619 | 620 | def _In(self): 621 | """Callback routine, which is called when data is available 622 | on the connection.""" 623 | connection = self.GetConnection() 624 | newData = connection.recv(100000) 625 | if len(newData) == 0: 626 | self.Close() 627 | if self._state != self._STATE_CONNECTED: 628 | self._state = self._STATE_DISCONNECTED 629 | else: 630 | erl_common.Debug(M, "InConnection: Connection broken") 631 | self._state = self._STATE_DISCONNECTED 632 | if self._tickTimers != None: 633 | self._tickTimers.Stop() 634 | self._connectionBrokenCb(self, self.GetPeerNodeName()) 635 | return 636 | 637 | self._recvdata = self._recvdata + newData 638 | remainingUnhandledData = self._HandleData(self._recvdata) 639 | self._recvdata = remainingUnhandledData 640 | 641 | def _HandleData(self, data): 642 | remainingInput = data 643 | while 1: 644 | if len(remainingInput) < self._packetLenSize: 645 | return remainingInput 646 | 647 | if self._packetLenSize == 2: 648 | packetLen = self.ReadInt2(remainingInput[0:2]) 649 | packetOffset = 2 650 | else: 651 | packetLen = self.ReadInt4(remainingInput[0:4]) 652 | packetOffset = 4 653 | 654 | if len(remainingInput) < self._packetLenSize + packetLen: 655 | return remainingInput 656 | 657 | packetData = remainingInput[packetOffset:packetOffset+packetLen] 658 | self._HandlePacket(packetData) 659 | remainingInput = remainingInput[packetOffset+packetLen:] 660 | 661 | def _HandlePacket(self, data): 662 | if self._state == self._STATE_HANDSHAKE_RECV_NAME: 663 | # First check that the correct message came in 664 | if chr(data[0]) != "n": 665 | erl_common.DebugHex(M, "handshake:recv_name", data) 666 | self.Close() 667 | self._state = self._STATE_DISCONNECTED 668 | self._peerDistrVersion = self.ReadInt2(data[1:3]) 669 | self._peerFlags = self.ReadInt4(data[3:7]) 670 | self._peerName = data[7:].decode("latin1") 671 | # FIXME: check for connections _to_ this node: 672 | # check whether nodeName > ownNodeName (or check < ?) 673 | self._SendStatusOk() 674 | self._SendChallenge() 675 | self._state = self._STATE_HANDSHAKE_RECV_CHALLENGE_REPLY 676 | elif self._state == self._STATE_HANDSHAKE_RECV_CHALLENGE_REPLY: 677 | # First check that the correct message came in 678 | if chr(data[0]) != "r": 679 | erl_common.DebugHex(M, "handshake:recv_chreply", data) 680 | self.Close() 681 | self._state = self._STATE_DISCONNECTED 682 | peersChallenge = self.ReadInt4(data[1:5]) 683 | peersDigest = data[5:] 684 | ownCookie = self._opts.GetCookie() 685 | if CheckDigest(peersDigest, self._challengeToPeer, ownCookie): 686 | self._SendChallengeAck(peersChallenge) 687 | self._packetLenSize = 4 688 | self._state = self._STATE_CONNECTED 689 | t = self._opts.GetNetTickTime() 690 | self._tickTimers = Ticker(t, self._Tick, self._NoResponse) 691 | self._newConnectionUpCb(self, self.GetPeerNodeName()) 692 | else: 693 | erl_common.Debug(M, 694 | "Connection attempt from disallowed node %s" % 695 | self._peerName) 696 | self.Close() 697 | self._state = self._STATE_DISCONNECTED 698 | elif self._state == self._STATE_CONNECTED: 699 | self._tickTimers.GotResonse() 700 | if len(data) == 0: 701 | # A tick 702 | return 703 | 704 | msgType = chr(data[0]) 705 | if msgType == "p": 706 | terms = erl_term.BinariesToTerms(data[1:]) 707 | if len(terms) == 2: 708 | controlMsg = terms[0] 709 | msg = terms[1] 710 | peerName = self.GetPeerNodeName() 711 | self._passThroughMsgCb(self, peerName, controlMsg, msg) 712 | elif len(terms) == 1: 713 | controlMsg = terms[0] 714 | peerName = self.GetPeerNodeName() 715 | self._passThroughMsgCb(self, peerName, controlMsg) 716 | else: 717 | debugTxt = "PassThrough-msg: terms=%s" % repr(terms) 718 | erl_common.DebugHex(M, debugTxt, data) 719 | else: 720 | erl_common.DebugHex(M, "msgType=%c" % msgType, data) 721 | else: 722 | erl_common.DebugHex(M, "state=%d" % self._state, data) 723 | 724 | 725 | def _Tick(self): 726 | """This callback is called by the Ticker class instance 727 | when it is time to send a tick to the other end, to indicate that 728 | we are still alive. 729 | """ 730 | self._SendPacket(b"") 731 | 732 | def _NoResponse(self): 733 | """This callback is called by the Ticker class instance 734 | when nothing has been received from the other end for too long. 735 | """ 736 | erl_common.Debug(M, "InConnection: Connection broken") 737 | self._state = self._STATE_DISCONNECTED 738 | self._tickTimers.Stop() 739 | self._connectionBrokenCb(self, self.GetPeerNodeName()) 740 | 741 | 742 | def _SendStatusOk(self): 743 | self._SendHandshakeMsg(b"sok") 744 | 745 | def _SendChallenge(self): 746 | challenge = GenChallenge() 747 | self._challengeToPeer = challenge 748 | packet = b"n" + \ 749 | self.PackInt2(self._opts.GetDistrVersion()) + \ 750 | self.PackInt4(self._opts.GetDistrFlags()) + \ 751 | self.PackInt4(challenge) + \ 752 | bytes(self._nodeName, "latin1") 753 | self._SendHandshakeMsg(packet) 754 | 755 | def _SendChallengeAck(self, challenge): 756 | packet = b"a" + GenDigest(challenge, self._opts.GetCookie()) 757 | self._SendHandshakeMsg(packet) 758 | 759 | def _SendHandshakeMsg(self, packet): 760 | msg = self.PackInt2(len(packet)) + packet 761 | erl_common.Debug(M, "Sending handshake") 762 | self.Send(msg) 763 | 764 | def _SendPacket(self, packet): 765 | msg = self.PackInt4(len(packet)) + packet 766 | erl_common.Debug(M, "Sending msg") 767 | self._tickTimers.RestartTick() 768 | self.Send(msg) 769 | -------------------------------------------------------------------------------- /py_interface/erl_opts.py: -------------------------------------------------------------------------------- 1 | ### py_interface -- A Python-implementation of an Erlang node 2 | ### 3 | ### $Id$ 4 | ### 5 | ### Copyright (C) 2002 Tomas Abrahamsson 6 | ### 7 | ### Author: Tomas Abrahamsson 8 | ### 9 | ### This file is part of the Py-Interface library 10 | ### 11 | ### This library is free software; you can redistribute it and/or 12 | ### modify it under the terms of the GNU Library General Public 13 | ### License as published by the Free Software Foundation; either 14 | ### version 2 of the License, or (at your option) any later version. 15 | ### 16 | ### This library is distributed in the hope that it will be useful, 17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | ### Library General Public License for more details. 20 | ### 21 | ### You should have received a copy of the GNU Library General Public 22 | ### License along with this library; if not, write to the Free 23 | ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | ### erl_opts.py -- holder class for options for the node 26 | 27 | ### Added a patch from Luke Gorrie, to support Erlang/OTP R10B, see 28 | ### http://article.gmane.org/gmane.comp.lang.erlang.general/9751 29 | 30 | 31 | DISTR_FLAG_PUBLISHED = 0x01 32 | DISTR_FLAG_ATOMCACHE = 0x02 33 | DISTR_FLAG_EXTENDEDREFERENCES = 0x04 34 | DISTR_FLAG_DISTMONITOR = 0x08 35 | DISTR_FLAG_FUNTAGS = 0x10 36 | DISTR_FLAG_DISTMONITORNAME = 0x20 37 | DISTR_FLAG_HIDDENATOMCACHE = 0x40 38 | DISTR_FLAG_NEWFUNTAGS = 0x80 39 | DISTR_FLAG_EXTENDEDPIDSPORTS = 0x100 40 | DISTR_FLAG_EXPORTPTRTAG = 0x200 41 | DISTR_FLAG_BITBINARIES = 0x400 42 | DISTR_FLAG_NEWFLOATS = 0x800 43 | DISTR_FLAG_UNICODEIO = 0x1000 44 | DISTR_FLAG_DISTHDRATOMCACHE = 0x2000 45 | DISTR_FLAG_SMALLATOMTAGS = 0x4000 46 | DISTR_FLAG_INTERNALTAGS = 0x8000 47 | DISTR_FLAG_UTF8ATOMS = 0x10000 48 | DISTR_FLAG_MAPTAG = 0x20000 49 | 50 | EPMD_PORT = 4369 51 | EPMD_HOST = "localhost" 52 | 53 | 54 | class ErlNodeOpts: 55 | def __init__(self, 56 | netTickTime=60, 57 | shortNodeNames=1, 58 | cookie="", 59 | distrVersion=5, 60 | epmdPort = EPMD_PORT, 61 | epmdHost = EPMD_HOST, 62 | distrFlags=(DISTR_FLAG_EXTENDEDREFERENCES| 63 | DISTR_FLAG_EXTENDEDPIDSPORTS| 64 | DISTR_FLAG_FUNTAGS| 65 | DISTR_FLAG_NEWFUNTAGS| 66 | DISTR_FLAG_EXPORTPTRTAG| 67 | DISTR_FLAG_BITBINARIES| 68 | DISTR_FLAG_NEWFLOATS| 69 | DISTR_FLAG_UTF8ATOMS| 70 | DISTR_FLAG_MAPTAG) 71 | ): 72 | self._netTickTime = netTickTime 73 | self._shortNodeNames = shortNodeNames 74 | self._cookie = cookie 75 | self._distrVersion = distrVersion 76 | self._distrFlags = distrFlags 77 | self._epmdPort = epmdPort 78 | self._epmdHost = epmdHost 79 | 80 | def GetNetTickTime(self): 81 | return self._netTickTime 82 | def SetNetTickTime(self, netTickTime): 83 | self._netTickTime = netTickTime 84 | 85 | def GetEpmdPort(self): 86 | return self._epmdPort 87 | def SetEpmdPort(self, epmdPort): 88 | self._epmdPort = epmdPort 89 | 90 | def GetEpmdHost(self): 91 | return self._epmdHost 92 | def SetEpmdHost(self, epmdHost): 93 | self._epmdHost = epmdHost 94 | 95 | def GetShortNodeNames(self): 96 | return self._shortNodeNames 97 | def SetShortNodeNames(self, shortNodeNames): 98 | self._shortNodeNames = shortNodeNames 99 | 100 | def GetCookie(self): 101 | return self._cookie 102 | def SetCookie(self, cookie): 103 | self._cookie = cookie 104 | 105 | def GetDistrVersion(self): 106 | return self._distrVersion 107 | def SetDistrVersion(self, distrVersion): 108 | self._distrVersion = distrVersion 109 | 110 | def GetDistrFlags(self): 111 | return self._distrFlags 112 | def SetDistrFlags(self, distrFlags): 113 | self._distrFlags = distrFlags 114 | -------------------------------------------------------------------------------- /setup.py.src: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='py_interface', 7 | version='@VSN@', 8 | description='A python-implementation of an Erlang node', 9 | author='Tomas Abrahamsson', 10 | author_email='tab@lysator.liu.se', 11 | url='http://www.lysator.liu.se/~tab/erlang/py_interface/', 12 | packages=['py_interface'], 13 | license='GNU Library General Public License') 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/Makefile.in: -------------------------------------------------------------------------------- 1 | ERL=@ERL@ 2 | ERLC=@ERLC@ 3 | 4 | SHELL=/bin/sh 5 | 6 | HAVE_MAPS:=@HAVE_MAPS@ 7 | HAVE_ERTS7_TIME_API:=@HAVE_ERTS7_TIME_API@ 8 | HAVE_EQC:=@HAVE_EQC@ 9 | HAVE_PROPER:=@HAVE_PROPER@ 10 | 11 | ERLC_FLAGS += -Wall +debug_info 12 | 13 | ERL_BATCH_FLAGS = +B -noshell -noinput 14 | 15 | # For easier development (assuming recent versions of Erlang): 16 | # Invert flags (eg NO_HAVE_MAPS rather than HAVE_MAPS), so 17 | # we can write -ifndef(NO_...). ... -endif... 18 | # This is so that when compiling in emacs, code for new features gets compiled 19 | # by default, so we won't have to add compilation flags by default. 20 | erlc_feature_flags = 21 | ifeq ($(HAVE_MAPS),no) 22 | erlc_feature_flags += -DNO_HAVE_MAPS 23 | endif 24 | ifeq ($(HAVE_ERTS7_TIME_API),no) 25 | erlc_feature_flags += -DNO_HAVE_ERTS7_TIME_API 26 | endif 27 | 28 | ifeq ($(HAVE_EQC),yes) 29 | HAVE_QC:=yes 30 | erlc_feature_flags += -DHAVE_QC -DHAVE_EQC -DQC=eqc 31 | else 32 | ifeq ($(HAVE_PROPER),yes) 33 | HAVE_QC:=yes 34 | erlc_feature_flags += -DHAVE_QC -DHAVE_PROPER -DQC=proper 35 | endif 36 | endif 37 | 38 | ERLS = pingpong_master_tests.erl \ 39 | py_node_mgr.erl 40 | ifeq ($(HAVE_QC),yes) 41 | ERLS += qc_pingpong_master_tests.erl 42 | endif 43 | 44 | ## Check verbosity 45 | ifdef VERBOSE 46 | verbose_opt := verbose 47 | pyunittest_opt := -v 48 | silencer := 49 | else 50 | verbose := 51 | pyunittest_opt := 52 | silencer := @ 53 | endif 54 | 55 | TEST_MODULES := \ 56 | $(filter %_tests,$(patsubst %.erl,%,$(ERLS))) 57 | 58 | SUPPORT_MODULES := \ 59 | $(filter-out $(TEST_MODULES),$(patsubst %.erl,%,$(ERLS))) 60 | 61 | BEAMS = $(patsubst %,%.beam,$(TEST_MODULES) $(SUPPORT_MODULES)) 62 | 63 | EUNIT_MODULES = $(TEST_MODULES) 64 | 65 | PYUNITTEST_MODULES = test_erl_term 66 | 67 | empty_str := 68 | space := $(empty_str) $(empty_str) 69 | comma := , 70 | 71 | all: $(BEAMS) 72 | 73 | test: all 74 | @echo Testing... 75 | @echo Python unit tests... 76 | $(silencer) python3 -m unittest $(pyunittest_opt) $(PYUNITTEST_MODULES) 77 | @echo Erlang unit tests... 78 | $(silencer)$(ERL) $(ERL_BATCH_FLAGS) -pa `pwd` \ 79 | -name pingpong_master-$$$$@127.0.0.1 \ 80 | -eval " \ 81 | case eunit:test([$(subst $(space),$(comma),$(EUNIT_MODULES))], \ 82 | [$(verbose_opt)]) of \ 83 | ok -> halt(0); \ 84 | _ -> halt(1) \ 85 | end." 86 | 87 | %.beam: %.erl 88 | $(ERLC) $(ERLC_FLAGS) $(erlc_feature_flags) $< 89 | 90 | clean: 91 | $(RM) *.beam erl_crash.dump 92 | -------------------------------------------------------------------------------- /test/out_connecting.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | ## A test case for testing packing/unpacking of erlang-terms: 4 | ## 5 | ## See run_test_erl_node_pingpong.sh for how to run it. 6 | ## 7 | ## A message is sent from an erlang node to a python node. 8 | ## That message is echoed back to the erlang node, which checks 9 | ## if the received message matches the original message. 10 | ## 11 | 12 | import os 13 | import sys 14 | import types 15 | import string 16 | import socket 17 | import getopt 18 | 19 | # expecting current dir to be test/ 20 | # so that we can find py_interface in .. 21 | sys.path.insert(0, "..") 22 | 23 | from py_interface import erl_term 24 | from py_interface import erl_node 25 | from py_interface import erl_opts 26 | from py_interface import erl_common 27 | from py_interface import erl_eventhandler 28 | 29 | mb = None 30 | quiet = False 31 | 32 | def __TestMBoxCallback(msg, *k, **kw): 33 | global mb, quiet 34 | if not quiet: 35 | txt = "Incoming msg=%s (k=%s, kw=%s)" % (repr(msg), repr(k), repr(kw)) 36 | print(txt.encode("ascii", errors="backslashreplace")) 37 | if type(msg) == tuple: 38 | if len(msg) == 2: 39 | if erl_term.IsErlPid(msg[0]): 40 | dest = msg[0] 41 | if not quiet: 42 | print("Sending it back to %s" % (dest,)) 43 | reply = msg[1] 44 | mb.Send(dest, (mb.Self(), reply)) 45 | 46 | def __FlushStdout(): 47 | input = sys.stdin.readline() 48 | if input == "": # master died 49 | sys.exit(0) 50 | print("-FLUSH-") 51 | sys.stdout.flush() 52 | 53 | def __EpmdConnectedOk(dest, atom_to_send): 54 | global mb 55 | msg = (mb.Self(), erl_term.ErlAtom(atom_to_send)) 56 | mb.Send(dest, msg) 57 | 58 | def main(argv): 59 | global mb, quiet 60 | 61 | try: 62 | opts, args = getopt.getopt(argv[1:], "?dn:c:q") 63 | except getopt.error as info: 64 | print(info) 65 | sys.exit(1) 66 | 67 | hostName = "localhost" 68 | ownNodeName = "py_interface_test" 69 | cookie = "cookie" 70 | doDebug = 0 71 | 72 | for (optchar, optarg) in opts: 73 | if optchar == "-?": 74 | print("Usage: %s erlnode" % argv[0]) 75 | sys.exit(1) 76 | elif optchar == "-c": 77 | cookie = optarg 78 | elif optchar == "-d": 79 | doDebug = 1 80 | elif optchar == "-q": 81 | quiet = 1 82 | elif optchar == "-n": 83 | ownNodeName = optarg 84 | 85 | if len(args) >= 3: 86 | pid_name_to_connect_to = args[0] 87 | node_to_connect_to = args[1] 88 | atom_to_send = args[2] 89 | else: 90 | print("Error: missing args: " + 91 | "pid_name_to_connect_to node_to_connect_to atom_to_send") 92 | sys.exit(1) 93 | 94 | if doDebug: 95 | erl_common.DebugOnAll() 96 | 97 | dest = (pid_name_to_connect_to, node_to_connect_to) 98 | 99 | print("Creating node...") 100 | n = erl_node.ErlNode(ownNodeName, erl_opts.ErlNodeOpts(cookie=cookie)) 101 | n.SetEpmdConnectedOkCb(erl_common.Callback(__EpmdConnectedOk, 102 | dest, atom_to_send)) 103 | print("Publishing node...") 104 | n.Publish() 105 | print("Creating mbox...") 106 | mb = n.CreateMBox(None) 107 | m = n.CreateMBox(__TestMBoxCallback) 108 | 109 | evhand = erl_eventhandler.GetEventHandler() 110 | evhand.PushReadEvent(sys.stdin, __FlushStdout) 111 | 112 | print("-GOING LOOPING-") # Erlang side will pick this text up 113 | sys.stdout.flush() 114 | 115 | evhand.Loop() 116 | 117 | try: 118 | main(sys.argv) 119 | except KeyboardInterrupt: 120 | print("Interrupted. Exiting.") 121 | sys.exit(1) 122 | -------------------------------------------------------------------------------- /test/pingpong_master_tests.erl: -------------------------------------------------------------------------------- 1 | %%% py_interface -- A Python-implementation of an Erlang node 2 | %%% 3 | %%% $Id$ 4 | %%% 5 | %%% Copyright (C) 2017 Tomas Abrahamsson 6 | %%% 7 | %%% Author: Tomas Abrahamsson 8 | %%% 9 | %%% This file is part of the Py-Interface library 10 | %%% 11 | %%% This library is free software; you can redistribute it and/or 12 | %%% modify it under the terms of the GNU Library General Public 13 | %%% License as published by the Free Software Foundation; either 14 | %%% version 2 of the License, or (at your option) any later version. 15 | %%% 16 | %%% This library is distributed in the hope that it will be useful, 17 | %%% but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | %%% Library General Public License for more details. 20 | %%% 21 | %%% You should have received a copy of the GNU Library General Public 22 | %%% License along with this library; if not, write to the Free 23 | %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | %%% pingpong_master_tests.py -- unit tests for ping-pong-ing msgs, master side 26 | -module(pingpong_master_tests). 27 | 28 | -include_lib("eunit/include/eunit.hrl"). 29 | 30 | -define(TIMEOUT, 30). 31 | 32 | -define(pp(Expr), {timeout, ?TIMEOUT, {spawn, fun() -> Expr end}}). 33 | 34 | -define(f(Fmt,Args), lists:flatten(io_lib:format(Fmt, Args))). 35 | 36 | -import(py_node_mgr, [pingpong_terms/1, pingpong_terms/2]). 37 | -import(py_node_mgr, [pingpong_terms_2/2]). 38 | 39 | check_can_fail_test_() -> 40 | ?pp(?assertError({received_unexpected, 41 | [{got, ok_deliberately_failed_as_you_wished}, 42 | {sent, do_deliberately_fail} | _]}, 43 | pingpong_terms([do_deliberately_fail], [quiet]))). 44 | 45 | small_integer_test_() -> 46 | ?pp(pingpong_terms([1, 255])). 47 | 48 | integer_test_() -> 49 | ?pp(pingpong_terms([256, 16#7fffffff])). 50 | 51 | negative_integer_test_() -> 52 | ?pp(pingpong_terms([-1, -256, -16#7fffffff, -16#80000000])). 53 | 54 | float_test_() -> 55 | ?pp(pingpong_terms([1.125])). 56 | 57 | atom_test_() -> 58 | ?pp(pingpong_terms([atom, ''])). 59 | 60 | reference_test_() -> 61 | ?pp(pingpong_terms([make_ref()])). 62 | 63 | port_test_() -> 64 | case erlang:ports() of 65 | [Port | _] -> ?pp(pingpong_terms([Port])); 66 | _ -> {timeout, ?TIMEOUT, fun() -> ok end} 67 | end. 68 | 69 | pid_test_() -> 70 | ?pp(pingpong_terms([self()])). 71 | 72 | small_tuple_test_() -> %% up to 255 elements 73 | ?pp(pingpong_terms([{}, {1}, {1,2,3,4}])). 74 | 75 | nil_test_() -> 76 | ?pp(pingpong_terms([[]])). 77 | 78 | string_test_() -> 79 | ?pp(pingpong_terms(["abc", lists:duplicate(3000, $a)])). 80 | 81 | list_test_() -> 82 | ?pp(pingpong_terms([[1,2,3]])). 83 | 84 | improper_list_test_() -> 85 | ?pp(pingpong_terms([[a|b]])). 86 | 87 | binary_test_() -> 88 | ?pp(pingpong_terms([<<>>, <<"abc">>])). 89 | 90 | bit_binary_test_() -> 91 | ?pp(pingpong_terms([<<1:2>>])). 92 | 93 | small_big_test_() -> 94 | ?pp(pingpong_terms([2 bsl 33])). 95 | 96 | negative_small_big_test_() -> 97 | ?pp(pingpong_terms([-16#80000001])). 98 | 99 | new_fun_test_() -> 100 | ?pp(pingpong_terms([fun() -> ok end, 101 | fun dummy/1])). 102 | 103 | dummy(X) -> X. 104 | 105 | export_test_() -> 106 | ?pp(pingpong_terms([fun lists:usort/1])). 107 | 108 | -ifndef(NO_HAVE_MAPS). 109 | map_test_() -> 110 | ?pp(pingpong_terms([#{}, #{a=>1}])). 111 | 112 | map_with_pymutable_obj_as_key_test_() -> 113 | ?pp(pingpong_terms([%% A list is mutable in Python, hence cannot 114 | %% normally occur as a dict key. 115 | #{[1,2] => a}, #{"abc" => [1,2]}, 116 | %% an ErlMapKey() object: 117 | #{make_ref() => 0}])). 118 | 'map_key_difference_==_vs_=:=_preserved_test_'() -> 119 | Map = #{0.0 => 1, 120 | 0 => 2}, 121 | 2 = maps:size(Map), 122 | ?pp(pingpong_terms([Map])). 123 | -endif. % NO_HAVE_MAPS 124 | 125 | large_tuple_test_() -> % >255 elements 126 | ?pp(pingpong_terms([list_to_tuple(lists:seq(0,257))])). 127 | 128 | large_big_ext_test_() -> % needs > 255 bytes, positive and negative 129 | ?pp(pingpong_terms([2 bsl (256*8), -(2 bsl (256*8))])). 130 | 131 | py_node_connects_test_() -> 132 | ?pp(py_node_connects_aux()). 133 | 134 | py_node_connects_aux() -> 135 | ProcName = mk_dummy_proc_name(), 136 | register(ProcName, self()), 137 | U = get_unique_integer(), 138 | A = list_to_atom(lists:concat(["t_msg_",U])), 139 | Opts = [{py_prog, "./out_connecting.py"}, 140 | {py_args, [atom_to_list(ProcName), 141 | atom_to_list(node()), 142 | atom_to_list(A)]}], 143 | py_node_mgr:ensure_started(Opts), 144 | receive 145 | {PyPid, A} -> 146 | pingpong_terms([abc123], [{py_pid, PyPid} | Opts]) 147 | after 10000 -> 148 | error({timeout_awaiting_msg_from_out_connecting_py_mode, 149 | [{got, flush()}, 150 | {opts, Opts}, 151 | {output, py_node_mgr:get_output(Opts)}]}) 152 | end. 153 | 154 | py_node_runs_rpc_test_() -> 155 | ?pp(py_node_runs_rpc_aux()). 156 | 157 | py_node_runs_rpc_aux() -> 158 | ProcName = mk_dummy_proc_name(), 159 | register(ProcName, self()), 160 | ToSum = [1,2,3,4], 161 | Sum = lists:sum(ToSum), 162 | Opts = [{py_prog, "./rpc_caller.py"}, 163 | {py_args, ["-r", atom_to_list(ProcName), 164 | atom_to_list(node()), 165 | "lists", 166 | "sum", 167 | ?f("[~p]", [ToSum])]}], 168 | py_node_mgr:ensure_started(Opts), 169 | receive 170 | {rpc_result, Sum} -> 171 | ok; 172 | {rpc_result, Other} -> 173 | error({got_unexpected_from_rpc_caller_py_mode, 174 | [{got, Other}, 175 | {opts, Opts}, 176 | {output, py_node_mgr:get_output(Opts)}, 177 | {flush, flush()}]}) 178 | after 10000 -> 179 | error({timeout_awaiting_rpc_result_from_rpc_caller_py_mode, 180 | [{got, flush()}, 181 | {opts, Opts}, 182 | {output, py_node_mgr:get_output(Opts)}]}) 183 | end. 184 | 185 | %% ------------ 186 | 187 | mk_dummy_proc_name() -> 188 | U = get_unique_integer(), 189 | Cand = list_to_atom(lists:concat([?MODULE,"_",U])), 190 | case whereis(Cand) of 191 | P when is_pid(P) -> mk_dummy_proc_name(); 192 | undefined -> Cand 193 | end. 194 | 195 | flush() -> 196 | receive 197 | Msg -> 198 | [Msg | flush()] 199 | after 0 -> 200 | [] 201 | end. 202 | 203 | -ifndef(NO_HAVE_ERTS7_TIME_API). 204 | get_unique_integer() -> 205 | erlang:unique_integer([positive]). 206 | -else. % NO_HAVE_ERTS7_TIME_API 207 | get_unique_integer() -> 208 | {N1,N2,N3} = now(), 209 | (N1 * 1000000 + N2) * 1000000 + N3. 210 | -endif. % NO_HAVE_ERTS7_TIME_API 211 | -------------------------------------------------------------------------------- /test/pingpong_slave.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | ## A test case for testing packing/unpacking of erlang-terms: 4 | ## 5 | ## See run_test_erl_node_pingpong.sh for how to run it. 6 | ## 7 | ## A message is sent from an erlang node to a python node. 8 | ## That message is echoed back to the erlang node, which checks 9 | ## if the received message matches the original message. 10 | ## 11 | 12 | import os 13 | import sys 14 | import types 15 | import string 16 | import socket 17 | import getopt 18 | 19 | # expecting current dir to be test/ 20 | # so that we can find py_interface in .. 21 | sys.path.insert(0, "..") 22 | 23 | from py_interface import erl_term 24 | from py_interface import erl_node 25 | from py_interface import erl_opts 26 | from py_interface import erl_common 27 | from py_interface import erl_eventhandler 28 | 29 | mb = None 30 | quiet = False 31 | 32 | deliberate_fail = erl_term.ErlAtom("do_deliberately_fail") 33 | as_you_wish = erl_term.ErlAtom("ok_deliberately_failed_as_you_wished") 34 | 35 | def __TestMBoxCallback(msg, *k, **kw): 36 | global mb, quiet 37 | if not quiet: 38 | txt = "Incoming msg=%s (k=%s, kw=%s)" % (repr(msg), repr(k), repr(kw)) 39 | print(txt.encode("ascii", errors="backslashreplace")) 40 | if type(msg) == tuple: 41 | if len(msg) == 2: 42 | if erl_term.IsErlPid(msg[0]): 43 | dest = msg[0] 44 | if not quiet: 45 | print("Sending it back to %s" % (dest,)) 46 | if msg[1] == deliberate_fail: 47 | reply = as_you_wish 48 | else: 49 | reply = msg[1] 50 | mb.Send(dest, (mb.Self(), reply)) 51 | 52 | def __FlushStdout(): 53 | input = sys.stdin.readline() 54 | if input == "": # master died 55 | sys.exit(0) 56 | print("-FLUSH-") 57 | sys.stdout.flush() 58 | 59 | def main(argv): 60 | global mb, quiet 61 | 62 | try: 63 | opts, args = getopt.getopt(argv[1:], "?dn:c:q") 64 | except getopt.error as info: 65 | print(info) 66 | sys.exit(1) 67 | 68 | hostName = "localhost" 69 | ownNodeName = "py_interface_test" 70 | cookie = "cookie" 71 | doDebug = 0 72 | 73 | for (optchar, optarg) in opts: 74 | if optchar == "-?": 75 | print("Usage: %s erlnode" % argv[0]) 76 | sys.exit(1) 77 | elif optchar == "-c": 78 | cookie = optarg 79 | elif optchar == "-d": 80 | doDebug = 1 81 | elif optchar == "-q": 82 | quiet = 1 83 | elif optchar == "-n": 84 | ownNodeName = optarg 85 | 86 | 87 | if doDebug: 88 | erl_common.DebugOnAll() 89 | 90 | print("Creating node...") 91 | n = erl_node.ErlNode(ownNodeName, erl_opts.ErlNodeOpts(cookie=cookie)) 92 | print("Publishing node...") 93 | n.Publish() 94 | print("Creating mbox...") 95 | mb = n.CreateMBox(None) 96 | m = n.CreateMBox(__TestMBoxCallback) 97 | print("Registering mbox as p...") 98 | m.RegisterName("p") 99 | 100 | evhand = erl_eventhandler.GetEventHandler() 101 | evhand.PushReadEvent(sys.stdin, __FlushStdout) 102 | 103 | print("-GOING LOOPING-") # Erlang side will pick this text up 104 | sys.stdout.flush() 105 | evhand.Loop() 106 | 107 | try: 108 | main(sys.argv) 109 | except KeyboardInterrupt: 110 | print("Interrupted. Exiting.") 111 | sys.exit(1) 112 | -------------------------------------------------------------------------------- /test/py_node_mgr.erl: -------------------------------------------------------------------------------- 1 | %%% py_interface -- A Python-implementation of an Erlang node 2 | %%% 3 | %%% $Id$ 4 | %%% 5 | %%% Copyright (C) 2017 Tomas Abrahamsson 6 | %%% 7 | %%% Author: Tomas Abrahamsson 8 | %%% 9 | %%% This file is part of the Py-Interface library 10 | %%% 11 | %%% This library is free software; you can redistribute it and/or 12 | %%% modify it under the terms of the GNU Library General Public 13 | %%% License as published by the Free Software Foundation; either 14 | %%% version 2 of the License, or (at your option) any later version. 15 | %%% 16 | %%% This library is distributed in the hope that it will be useful, 17 | %%% but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | %%% Library General Public License for more details. 20 | %%% 21 | %%% You should have received a copy of the GNU Library General Public 22 | %%% License along with this library; if not, write to the Free 23 | %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | %%% py_node_mgr.py -- manage a python nodes 26 | -module(py_node_mgr). 27 | 28 | -export([pingpong_terms/1, pingpong_terms/2]). 29 | 30 | -export([ensure_started/0, ensure_started/1]). 31 | -export([ensure_stopped/0, ensure_stopped/1]). 32 | -export([get_output/0, get_output/1]). 33 | -export([set_master/1, set_master/2]). 34 | -export([pingpong_term_with_started/2]). 35 | -export([get_state/0, get_state/1]). % for debugging 36 | 37 | -export([start_py_node/1]). % for debugging 38 | -export([stop_py_node/1]). % for debugging 39 | 40 | -include_lib("eunit/include/eunit.hrl"). 41 | 42 | %% one-shot-api 43 | pingpong_terms(Terms) -> pingpong_terms(Terms, []). 44 | pingpong_terms(Terms, Opts) -> 45 | {ok, NodeName} = ensure_started(), 46 | set_master(infinity_sleeper()), 47 | try 48 | [pingpong_term_with_started(NodeName, Term) || Term <- Terms], 49 | ok 50 | catch Class:Reason -> 51 | St = erlang:get_stacktrace(), 52 | ensure_stopped(), 53 | case Reason of 54 | {_, Infos} when is_list(Infos) -> 55 | Output = proplists:get_value(output, Infos), 56 | case proplists:get_bool(quiet, Opts) of 57 | true -> 58 | ok; 59 | false -> 60 | io:format( 61 | user, 62 | "~nPyOut = vvvv------------~n" 63 | "~s~n" 64 | "^^^^--------------------~n", 65 | [Output]) 66 | end; 67 | _ -> 68 | ok 69 | end, 70 | erlang:raise(Class,Reason,St) 71 | end. 72 | 73 | infinity_sleeper() -> 74 | case whereis(infinity_sleeper) of 75 | undefined -> 76 | P = spawn(timer,sleep,[infinity]), 77 | register(infinity_sleeper, P), 78 | P; 79 | P when is_pid(P) -> 80 | P 81 | end. 82 | 83 | %% ------------------ 84 | %% Server-API 85 | 86 | %% Monitor first starter, terminate if it dies. 87 | -spec ensure_started() -> {ok, NodeName::atom()}. 88 | ensure_started() -> ensure_started([]). 89 | ensure_started(Opts) -> 90 | MgrName = mgr_name(Opts), 91 | ensure_mgr_process_running(MgrName, Opts), 92 | call(MgrName, ensure_started). % to get node name 93 | 94 | -spec ensure_stopped() -> ok. 95 | ensure_stopped() -> ensure_stopped([]). 96 | ensure_stopped(Opts) -> 97 | try 98 | call(mgr_name(Opts), ensure_stopped) 99 | catch error:badarg -> 100 | %% message sending to ?SERVER presumably failed, hence stopped 101 | ok 102 | end. 103 | 104 | -spec get_output() -> binary(). 105 | get_output() -> get_output([]). 106 | get_output(Opts) -> 107 | call(mgr_name(Opts), get_output). 108 | 109 | %% Monitor NewMaster, terminate if it dies. 110 | -spec set_master(pid()) -> ok. 111 | set_master(NewMaster) -> set_master(NewMaster, []). 112 | set_master(NewMaster, Opts) -> 113 | call(mgr_name(Opts), {set_master, NewMaster}). 114 | 115 | -spec pingpong_term_with_started(atom(), term()) -> ok. 116 | pingpong_term_with_started(NodeName, What) -> 117 | pingpong_term_with_started(NodeName, What, []). 118 | pingpong_term_with_started(NodeName, Term, Opts) -> 119 | Dest = proplists:get_value(py_pid, Opts, {p, NodeName}), 120 | Dest ! {self(), Term}, 121 | receive 122 | {OtherP, Term} when is_pid(OtherP), node(OtherP) /= node() -> 123 | _ = get_output(), % drop it so we don't acc to much, if later fail 124 | ok; 125 | {OtherP, OtherTerm} when is_pid(OtherP), node(OtherP) /= node() -> 126 | Output = get_output(), 127 | error({received_unexpected, [{got, OtherTerm}, {sent, Term}, 128 | {output, Output}]}); 129 | Other -> 130 | Output = get_output(), 131 | error({received_unexpected, [{got, Other}, {sent, Term}, 132 | {output, Output}]}) 133 | after sensible_transmission_and_processing_timeout(Term) -> 134 | error({timeout, [{sent, Term}, 135 | {output, get_output()}]}) 136 | end. 137 | 138 | sensible_transmission_and_processing_timeout(Term) -> 139 | %% Quickcheck tests can be huge, and I've actually seen it time out 140 | %% unnecessarily (probably) on a slow host. So make the timeout 141 | %% proportional to the "size" of Term. How to measure size? There's 142 | %% erts_term:[flat_]size, but it is undocumented. Perhaps equally fine is 143 | %% to just check the size of the term as a binary. 144 | 145 | %% Let's say an extra half second per kB 146 | ExtraTime = 500, 147 | PerSize = 1000, 148 | TimeFactor = ExtraTime / PerSize, 149 | 150 | round(5000 + byte_size(term_to_binary(Term)) * TimeFactor). 151 | 152 | get_state() -> get_state([]). 153 | get_state(Opts) -> 154 | call(mgr_name(Opts), get_state). 155 | 156 | %% ------------------- 157 | ensure_mgr_process_running(Name, Opts) -> 158 | case whereis(Name) of 159 | undefined -> 160 | Master = self(), 161 | ok = proc_lib:start( 162 | erlang, apply, 163 | [fun() -> 164 | try register(Name, self()) of 165 | _ -> 166 | init(Master, Opts) 167 | catch error:badarg -> 168 | ok %% startup-race 169 | end 170 | end, []]); 171 | P when is_pid(P) -> 172 | ok 173 | end. 174 | 175 | -record(pynode, {port :: port(), 176 | node_name :: atom(), 177 | os_pid :: non_neg_integer()}). 178 | -record(state, {py_node :: #pynode{}, 179 | mref :: reference(), 180 | opts :: [term()]}). 181 | 182 | init(Master, Opts) -> 183 | MRef = monitor(process, Master), 184 | {ok, PyNode} = start_py_node(Opts), 185 | proc_lib:init_ack(ok), 186 | loop(#state{py_node = PyNode, mref = MRef, opts = Opts}, []). 187 | 188 | %% State: py_node is running normally 189 | loop(#state{py_node=#pynode{port=Port, node_name=NodeName}=PyNode, 190 | mref=MRef}=State, 191 | AccOutput) -> 192 | receive 193 | {Port, {data, Txt}} -> 194 | loop(State, [Txt | AccOutput]); 195 | {Port, {exit_status, _ExitStatus}} -> 196 | loop_terminated(State, io_finalize(AccOutput)); 197 | {'DOWN', MRef, _, _, _} -> 198 | stop_py_node(PyNode), 199 | exit(normal); 200 | {call, Req, From} -> 201 | case Req of 202 | {set_master, NewMaster} -> 203 | demonitor(MRef, [flush]), 204 | MRef1 = monitor(process, NewMaster), 205 | reply(From, ok), 206 | loop(State#state{mref = MRef1}, AccOutput); 207 | get_output -> 208 | case sync_py_node(PyNode) of 209 | {ok, Resp} -> 210 | reply(From, io_finalize([Resp | AccOutput])), 211 | loop(State, []); 212 | {error, Output} -> 213 | reply(From, io_finalize([Output | AccOutput])), 214 | loop_terminated(State, []) 215 | end; 216 | ensure_started -> 217 | reply(From, {ok, NodeName}), 218 | loop(State, AccOutput); 219 | ensure_stopped -> 220 | stop_py_node(PyNode), 221 | loop_stopping(State, From, AccOutput); 222 | get_state -> 223 | reply(From, {loop, State, AccOutput}), 224 | loop(State, AccOutput) 225 | end; 226 | X -> 227 | io:format("Unexpected msg (ignored):~n ~p~n", [X]), 228 | loop(State, AccOutput) 229 | end. 230 | 231 | %% State: py_node has terminated 232 | loop_terminated(#state{mref=MRef, opts=Opts}=State, Output) -> 233 | receive 234 | {'DOWN', MRef, _, _, _} -> 235 | exit(normal); 236 | {call, Req, From} -> 237 | case Req of 238 | {set_master, NewMaster} -> 239 | demonitor(MRef, [flush]), 240 | MRef1 = monitor(process, NewMaster), 241 | reply(From, ok), 242 | loop_terminated(State#state{mref = MRef1}, Output); 243 | get_output -> 244 | reply(From, Output), 245 | loop_terminated(State, <<>>); 246 | ensure_started -> 247 | {ok, #pynode{node_name=NodeName}=PyNode} = 248 | start_py_node(Opts), 249 | reply(From, {ok, NodeName}), 250 | loop(State#state{py_node = PyNode}, []); 251 | ensure_stopped -> 252 | reply(From, ok), 253 | loop_terminated(State, Output); 254 | get_state -> 255 | reply(From, {terminated, State, Output}), 256 | loop_terminated(State, Output) 257 | end; 258 | X -> 259 | io:format("Unexpected msg (ignored):~n ~p~n", [X]), 260 | loop_terminated(State, Output) 261 | end. 262 | 263 | %% Collect pending output, then go to terminated 264 | loop_stopping(#state{py_node=#pynode{port=Port}, 265 | mref=MRef}=State, 266 | From, 267 | AccOutput) -> 268 | receive 269 | {Port, {data, Txt}} -> 270 | loop_stopping(State, From, [Txt | AccOutput]); 271 | {Port, {exit_status, _ExitStatus}} -> 272 | reply(From, ok), 273 | loop_terminated(State, io_finalize(AccOutput)); 274 | {'DOWN', MRef, _, _, _} -> 275 | exit(normal) 276 | end. 277 | 278 | sync_py_node(#pynode{port=Port}=PyNode) -> 279 | port_command(Port, "\n"), 280 | read_till_flush(PyNode, []). 281 | 282 | read_till_flush(#pynode{port=Port}=PyNode, AccOutput) -> 283 | receive 284 | {Port, {data, Txt}} -> 285 | AllOut = io_finalize([Txt | AccOutput]), 286 | case binary:match(AllOut, <<"-FLUSH-">>) of 287 | nomatch -> 288 | read_till_flush(PyNode, [AllOut]); 289 | {_Start, _Len} -> 290 | {ok, AllOut} 291 | end; 292 | {Port, {exit_status, _ExitStatus}} -> 293 | {error, io_finalize(AccOutput)} 294 | end. 295 | 296 | io_finalize(Acc) -> iolist_to_binary(lists:reverse(Acc)). 297 | 298 | call(Name, Req) -> 299 | MRef = monitor(process, Name), 300 | Name ! {call, Req, {MRef, self()}}, 301 | receive 302 | {reply, MRef, Res} -> 303 | demonitor(MRef, [flush]), 304 | Res; 305 | {'DOWN', MRef, _, _, Reason} -> 306 | exit({terminated,Reason}) 307 | end. 308 | 309 | reply({Ref, Client}, Res) -> 310 | Client ! {reply, Ref, Res}. 311 | 312 | mgr_name(Opts) -> 313 | list_to_atom(lists:concat(["mgr_for_", get_py_name(Opts)])). 314 | 315 | get_py_name(Opts) -> 316 | filename:basename(get_py_prog(Opts), ".py"). 317 | 318 | get_py_prog(Opts) -> 319 | proplists:get_value(py_prog, Opts, "./pingpong_slave.py"). 320 | 321 | start_py_node(Opts) -> 322 | PyName = get_py_name(Opts), 323 | PyProg = get_py_prog(Opts), 324 | PyArgs = proplists:get_value(py_args, Opts, []), 325 | SelfOsPid = os:getpid(), 326 | NodeName = mk_unused_node_name(lists:concat( 327 | [PyName, "-for-", SelfOsPid, "-"])), 328 | P = open_port({spawn_executable, PyProg}, 329 | [use_stdio, stderr_to_stdout, binary, 330 | exit_status, 331 | {env, [{"PYTHONDONTWRITEBYTECODE", "x"}, 332 | {"PYTHONUNBUFFERED", "x"}]}, 333 | {args,["-n", atom_to_list(NodeName), 334 | "-c", erlang:get_cookie(), 335 | "-d" 336 | | PyArgs]}]), 337 | {os_pid,OsPid} = erlang:port_info(P, os_pid), 338 | PyNode = #pynode{port = P, 339 | node_name = NodeName, 340 | os_pid = OsPid}, 341 | await_py_node_up(PyNode), 342 | {ok, PyNode}. 343 | 344 | mk_unused_node_name(Prefix) -> 345 | U = get_unique_integer(), 346 | Base = lists:concat([Prefix, U]), 347 | case erl_epmd:names() of 348 | {ok, LocalNames} -> 349 | case lists:keymember(Base, 1, LocalNames) of 350 | true -> 351 | mk_unused_node_name(Prefix); 352 | false -> 353 | list_to_atom(lists:concat([Base, "@127.0.0.1"])) 354 | end; 355 | _ -> 356 | list_to_atom(lists:concat([Base, "@127.0.0.1"])) 357 | end. 358 | 359 | await_py_node_up(PyNode) -> 360 | MaxT = calc_max_t(timer:seconds(5)), 361 | await_py_node_going_looping(PyNode, MaxT, []), 362 | await_py_node_in_epmd(PyNode, MaxT). 363 | 364 | await_py_node_going_looping(#pynode{port=Port}=PyNode, MaxT, Acc) -> 365 | case collect_output_aux(Port, Acc) of 366 | {undefined, Txt} -> % still running 367 | case binary:match(Txt, <<"-GOING LOOPING-">>) of 368 | nomatch -> 369 | case has_timed_out(MaxT) of 370 | true -> error({timeout_awaiting_going_looping, 371 | [{state, still_running}, 372 | {output, Txt}]}); 373 | false -> 374 | timer:sleep(25), 375 | await_py_node_going_looping(PyNode, MaxT, [Txt]) 376 | end; 377 | {_Start, _Len} -> 378 | ok 379 | end; 380 | {ExitStatus, Txt} -> 381 | error({py_node_terminated_prematurely, 382 | [{exit_status, ExitStatus}, 383 | {output, Txt}]}) 384 | end. 385 | 386 | await_py_node_in_epmd(#pynode{node_name=NodeName}=PyNode, MaxT) -> 387 | UpToAt = lists:takewhile(fun(C) -> C /= $@ end, atom_to_list(NodeName)), 388 | {ok, Names} = erl_epmd:names(), % expecting epmd to be started 389 | case lists:keymember(UpToAt, 1, Names) of 390 | true -> 391 | ok; 392 | false -> 393 | case has_timed_out(MaxT) of 394 | true -> error({timeout_awaiting_node_in_epmd, 395 | [{names_in_epmd, Names}]}); 396 | false -> 397 | timer:sleep(25), 398 | await_py_node_in_epmd(PyNode, MaxT) 399 | end 400 | end. 401 | 402 | stop_py_node(#pynode{os_pid=OsPid}) -> 403 | os:cmd("kill -9 "++integer_to_list(OsPid)). 404 | 405 | collect_output_aux(Port, Acc) -> 406 | receive 407 | {Port, {data, Txt}} -> 408 | collect_output_aux(Port, [Txt | Acc]); 409 | {Port, {exit_status,ExitStatus}} -> 410 | {ExitStatus, iolist_to_binary(lists:reverse(Acc))} 411 | after 50 -> 412 | {undefined, iolist_to_binary(lists:reverse(Acc))} 413 | end. 414 | 415 | -ifndef(NO_HAVE_ERTS7_TIME_API). 416 | get_unique_integer() -> 417 | erlang:unique_integer([positive]). 418 | 419 | calc_max_t(MillisFromNow) -> 420 | erlang:monotonic_time(millisecond) + MillisFromNow. 421 | 422 | has_timed_out(MaxT) -> 423 | erlang:monotonic_time(millisecond) > MaxT. 424 | 425 | -else. % NO_HAVE_ERTS7_TIME_API 426 | get_unique_integer() -> 427 | {N1,N2,N3} = now(), 428 | (N1 * 1000000 + N2) * 1000000 + N3. 429 | 430 | calc_max_t(MaxT) -> 431 | {now(),MaxT}. 432 | 433 | has_timed_out({T0,MaxT}) -> 434 | (timer:now_diff(now(),T0) / 1000) > MaxT. 435 | 436 | -endif. % NO_HAVE_ERTS7_TIME_API 437 | -------------------------------------------------------------------------------- /test/qc_pingpong_master_tests.erl: -------------------------------------------------------------------------------- 1 | %%% py_interface -- A Python-implementation of an Erlang node 2 | %%% 3 | %%% $Id$ 4 | %%% 5 | %%% Copyright (C) 2017 Tomas Abrahamsson 6 | %%% 7 | %%% Author: Tomas Abrahamsson 8 | %%% 9 | %%% This file is part of the Py-Interface library 10 | %%% 11 | %%% This library is free software; you can redistribute it and/or 12 | %%% modify it under the terms of the GNU Library General Public 13 | %%% License as published by the Free Software Foundation; either 14 | %%% version 2 of the License, or (at your option) any later version. 15 | %%% 16 | %%% This library is distributed in the hope that it will be useful, 17 | %%% but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | %%% Library General Public License for more details. 20 | %%% 21 | %%% You should have received a copy of the GNU Library General Public 22 | %%% License along with this library; if not, write to the Free 23 | %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 24 | 25 | %%% pingpong_master_tests.py -- quickcheck/proper tests 26 | %%% for ping-pong-ing msgs, master side 27 | -module(qc_pingpong_master_tests). 28 | 29 | -define(TIMEOUT, 180). % extra time needed in case of shrinking, on slow host 30 | 31 | -export([prop_same_term_returns_after_roundtrip/1]). 32 | 33 | -ifdef(HAVE_EQC). 34 | -include_lib("eqc/include/eqc.hrl"). 35 | -define(qc,eqc). 36 | -else. 37 | -ifdef(HAVE_PROPER). 38 | -include_lib("proper/include/proper.hrl"). 39 | -define(qc,proper). 40 | -endif. 41 | -endif. 42 | 43 | -include_lib("eunit/include/eunit.hrl"). 44 | 45 | prop_same_term_returns_after_roundtrip_test_() -> 46 | {timeout,?TIMEOUT, 47 | fun() -> 48 | py_node_mgr:ensure_started(), 49 | ?assert( 50 | ?qc:quickcheck( 51 | ?qc:numtests( 52 | 50, 53 | prop_same_term_returns_after_roundtrip(self())))), 54 | py_node_mgr:ensure_stopped(), 55 | ok 56 | end}. 57 | 58 | prop_same_term_returns_after_roundtrip(Master) -> 59 | ?FORALL(Term, t_term(), 60 | begin 61 | {_P,MRef} = do_spawn_pingponger(Master, Term), 62 | do_collect_pingponger_reply(MRef) 63 | end). 64 | 65 | do_spawn_pingponger(Master, Term) -> 66 | spawn_monitor( 67 | fun() -> 68 | try 69 | {ok,NodeName} = py_node_mgr:ensure_started(), 70 | py_node_mgr:set_master(Master), 71 | py_node_mgr:pingpong_term_with_started(NodeName, Term) 72 | catch Class:Reason -> 73 | St = erlang:get_stacktrace(), 74 | py_node_mgr:ensure_stopped(), 75 | exit({fail,Term,Class,Reason,St}) 76 | end 77 | end). 78 | 79 | do_collect_pingponger_reply(MRef) -> 80 | receive 81 | {'DOWN', MRef, _, _, Reason} -> 82 | case Reason of 83 | normal -> 84 | true; 85 | {fail,_Term,Class,Reason2,St} -> 86 | erlang:raise(Class,Reason2,St) 87 | end 88 | end. 89 | 90 | t_term() -> 91 | oneof([t_term2(), 92 | list(t_term2()), 93 | t_tuple(n_uint(14), t_term2())]). 94 | 95 | t_term2() -> 96 | oneof([int(), n_uint(32), n_sint(32), largeint(), 97 | real(), 98 | t_atom(), 99 | t_ref(), 100 | t_pid(), 101 | t_string(), 102 | binary(), 103 | bitstring(), 104 | t_fun(), 105 | t_improper_list(), 106 | t_port(), 107 | t_map()]). 108 | 109 | n_sint(Base) -> 110 | oneof([n_uint(Base), ?LET(X, n_uint(Base), -X)]). 111 | 112 | n_uint(Base) -> 113 | oneof([choose(0, pow2(B)-1) || B <- lists:seq(1, Base)]). 114 | 115 | pow2(0) -> 1; 116 | pow2(N) when N > 0 -> 2*pow2(N-1); 117 | pow2(N) when N < 0 -> 1/pow2(-N). 118 | 119 | t_tuple(SizeG, G) -> 120 | ?LET(NumElems, SizeG, 121 | ?LET(L, vector(NumElems, G), list_to_tuple(L))). 122 | 123 | t_atom() -> 124 | oneof([a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, 125 | a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, 126 | a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, 127 | x]). 128 | 129 | t_ref() -> 130 | ?LET(_, int(), make_ref()). 131 | 132 | t_pid() -> 133 | ?LET(_, int(), self()). 134 | 135 | t_port() -> 136 | elements(erlang:ports()). 137 | 138 | 139 | t_string() -> 140 | list(n_uint(8)). 141 | 142 | t_fun() -> 143 | oneof([fun dummy/0, 144 | fun() -> ok end, 145 | fun lists:reverse/1]). 146 | 147 | dummy() -> 148 | ok. 149 | 150 | t_improper_list() -> 151 | ?LET({E1,E2}, {t_atom(), t_atom()}, [E1|E2]). 152 | 153 | -ifndef(NO_HAVE_MAPS). 154 | t_map() -> 155 | ?LET(NumElems, n_uint(2), 156 | ?LET({Keys, Values}, {vector(NumElems, t_term2()), 157 | vector(NumElems, t_term2())}, 158 | maps:from_list(lists:zip(Keys, Values)))). 159 | -else. % NO_HAVE_MAPS 160 | t_map() -> 161 | t_atom(). % whatever 162 | -endif. % NO_HAVE_MAPS 163 | -------------------------------------------------------------------------------- /test/rpc_caller.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | ## A test case for testing packing/unpacking of erlang-terms: 4 | ## 5 | ## See run_test_erl_node_pingpong.sh for how to run it. 6 | ## 7 | ## A message is sent from an erlang node to a python node. 8 | ## That message is echoed back to the erlang node, which checks 9 | ## if the received message matches the original message. 10 | ## 11 | 12 | import os 13 | import sys 14 | import types 15 | import string 16 | import socket 17 | import getopt 18 | 19 | # expecting current dir to be test/ 20 | # so that we can find py_interface in .. 21 | sys.path.insert(0, "..") 22 | 23 | from py_interface import erl_term 24 | from py_interface import erl_node 25 | from py_interface import erl_opts 26 | from py_interface import erl_common 27 | from py_interface import erl_eventhandler 28 | 29 | mb = None 30 | quiet = False 31 | 32 | def __TestMBoxCallback(msg, *k, **kw): 33 | global mb, quiet 34 | if not quiet: 35 | txt = "Incoming msg=%s (k=%s, kw=%s)" % (repr(msg), repr(k), repr(kw)) 36 | print(txt.encode("ascii", errors="backslashreplace")) 37 | if type(msg) == tuple: 38 | if len(msg) == 2: 39 | if erl_term.IsErlPid(msg[0]): 40 | dest = msg[0] 41 | if not quiet: 42 | print("Sending it back to %s" % (dest,)) 43 | reply = msg[1] 44 | mb.Send(dest, (mb.Self(), reply)) 45 | 46 | def __FlushStdout(): 47 | input = sys.stdin.readline() 48 | if input == "": # master died 49 | sys.exit(0) 50 | print("-FLUSH-") 51 | sys.stdout.flush() 52 | 53 | def __EpmdConnectedOk(node, mod, fn, args, cb): 54 | global mb 55 | mb.SendRPC(node, mod, fn, args, cb) 56 | 57 | node_to_connect_to = None 58 | res_pidname = None 59 | 60 | def __RpcResult(res): 61 | global res_pidname, node_to_connect_to 62 | print("RESULT:%s" % repr(res)) 63 | if res_pidname != None: 64 | dest = (res_pidname, node_to_connect_to) 65 | msg = (erl_term.ErlAtom("rpc_result"), res) 66 | mb.Send(dest, msg) 67 | 68 | def main(argv): 69 | global mb, quiet, node_to_connect_to 70 | 71 | try: 72 | opts, args = getopt.getopt(argv[1:], "?dn:c:qr:") 73 | except getopt.error as info: 74 | print(info) 75 | sys.exit(1) 76 | 77 | hostName = "localhost" 78 | ownNodeName = "py_interface_test" 79 | cookie = "cookie" 80 | doDebug = 0 81 | 82 | for (optchar, optarg) in opts: 83 | if optchar == "-?": 84 | print("Usage: %s erlnode" % argv[0]) 85 | sys.exit(1) 86 | elif optchar == "-c": 87 | cookie = optarg 88 | elif optchar == "-d": 89 | doDebug = 1 90 | elif optchar == "-q": 91 | quiet = 1 92 | elif optchar == "-n": 93 | ownNodeName = optarg 94 | elif optchar == "-r": 95 | global res_pidname 96 | res_pidname = optarg 97 | 98 | if len(args) >= 4: 99 | node_to_connect_to = args[0] 100 | mod = args[1] 101 | fn = args[2] 102 | args = eval(args[3]) 103 | else: 104 | print("Error: missing args: " + 105 | "node_to_connect_to mod fn args") 106 | sys.exit(1) 107 | 108 | if doDebug: 109 | erl_common.DebugOnAll() 110 | 111 | print("Creating node...") 112 | n = erl_node.ErlNode(ownNodeName, erl_opts.ErlNodeOpts(cookie=cookie)) 113 | n.SetEpmdConnectedOkCb(erl_common.Callback(__EpmdConnectedOk, 114 | node_to_connect_to, 115 | mod, fn, args, 116 | __RpcResult)) 117 | print("Publishing node...") 118 | n.Publish() 119 | print("Creating mbox...") 120 | mb = n.CreateMBox(None) 121 | m = n.CreateMBox(__TestMBoxCallback) 122 | 123 | evhand = erl_eventhandler.GetEventHandler() 124 | evhand.PushReadEvent(sys.stdin, __FlushStdout) 125 | 126 | print("-GOING LOOPING-") # Erlang side will pick this text up 127 | sys.stdout.flush() 128 | 129 | evhand.Loop() 130 | 131 | try: 132 | main(sys.argv) 133 | except KeyboardInterrupt: 134 | print("Interrupted. Exiting.") 135 | sys.exit(1) 136 | -------------------------------------------------------------------------------- /test/test_erl_term.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import unittest 6 | 7 | # expecting current dir to be test/ 8 | # so that we can find py_interface in .. 9 | sys.path.insert(0, "..") 10 | 11 | from py_interface.erl_term import BinaryToTerm, TermToBinary, \ 12 | IODataToStr, StrToList 13 | 14 | ## BF = fun(T) -> io:format("b\"~s\"~n", [[io_lib:format("\\x~2.16.0b",[C]) || <> <= term_to_binary(T)]]) end. 15 | 16 | class TestString(unittest.TestCase): 17 | def test_unpack_abc(self): 18 | b = b"\x83\x6b\x00\x03\x61\x62\x63" # "abc" 19 | self.assertEqual(BinaryToTerm(b), 'abc') 20 | self.assertEqual(TermToBinary('abc'), b) 21 | 22 | def test_unpack_latin1_diacritic_chars(self): 23 | b = b"\x83\x6b\x00\x03\xe5\xe4\xf6" # "åäö" (as latin1 chars) 24 | self.assertEqual(BinaryToTerm(b), 'åäö') 25 | self.assertEqual(TermToBinary('åäö'), b) 26 | 27 | def test_iodata_to_list_and_back(self): 28 | self.assertEqual(IODataToStr([]), '') 29 | self.assertEqual(IODataToStr([[[],[]]]), '') 30 | self.assertEqual(IODataToStr([ord("a"),ord("b"), ord("c")]), 'abc') 31 | self.assertEqual(IODataToStr(b"abc"), 'abc') 32 | self.assertEqual(IODataToStr(["a",0xe5]), "aå") 33 | self.assertEqual(StrToList("abc"), [97,98,99]) 34 | 35 | class TestBinary(unittest.TestCase): 36 | def test_unpack_binary(self): 37 | b = b"\x83\x6d\x00\x00\x00\x03\x61\x62\x63" # <<"abc">> 38 | self.assertEqual(BinaryToTerm(b), b'abc') 39 | self.assertEqual(TermToBinary(b'abc'), b) 40 | 41 | def test_unpack_latin1_diacritic_chars(self): 42 | b = b"\x83\x6d\x00\x00\x00\x03\xe5\xe4\xf6" # <<"åäö">> (latin1 chars) 43 | self.assertEqual(BinaryToTerm(b), b'\xe5\xe4\xf6') 44 | self.assertEqual(TermToBinary(b'\xe5\xe4\xf6'), b) 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /vsn: -------------------------------------------------------------------------------- 1 | 2.3 2 | --------------------------------------------------------------------------------