.. _devnotes: =============== Developer Notes =============== YaPyCon is basically doing two things: 1. It launches a `QT Console for Jupyter `_ **in its own process**. 2. It launches a Remote Procedure Call "server" (**in its own thread**) and links the context of the console with the context of the plugin. Having a program launch a Python console to provide scripting capabilities to another program is the least difficult part of this endeavour, thanks to projects like `qtconsole `_. The most "challenging" part is #2 and the main reason why YaPyCon works and "looks" the way it does. To understand why, it is essential to understand how YASARA launches a plugin and how does that plugin communicate back with the main process that launched it. YASARA - Plugin communications ============================== Let us first have a look at what happens when YASARA launches a plugin. Here is a diagram with all the key actors involved: .. mermaid:: :caption: Simplified sequence diagram of the plugin launching process. sequenceDiagram participant YASARA participant Plugin participant yasara.py participant Local_Socket_Server YASARA ->>Plugin: Launch plugin with
request (r), listen on
stdout for
Yanaconda commands. Plugin ->>yasara.py: import yasara yasara.py ->>yasara.py: Initialise plugin variables yasara.py ->>yasara.py: Discover a free port (p) yasara.py ->>Local_Socket_Server: Launch on port p
yasara.py ->>YASARA: Pass p back to YASARA
(as part of calling ``LoadStorage``) yasara.py ->>Plugin: Import complete,
return to plugin
code execution Plugin ->>Plugin: Examine (r),
proceed accordingly. And, having described the initialisation part, let us now try to "call" a YASARA command from within Python, for example ``ListAtom()`` : .. mermaid:: :caption: Simplified sequence diagram of "calling" a YASARA command with return results. sequenceDiagram participant YASARA participant Plugin participant yasara.py participant Local_Socket_Server Plugin->>yasara.py: yasara.ListAtom("all") yasara.py->>yasara.py: Create equivalent
Yanaconda ListAtom
command (c) yasara.py->>YASARA: Send c via stdout YASARA->>YASARA: Execute ListAtom, obtain result (r) YASARA->>Local_Socket_Server: Connect to p, send r yasara.py->>Local_Socket_Server: Read r yasara.py->>Plugin: return r Remarks ------- The key points to note here are: 1. YASARA accepts commands about what to do via the ``stdout`` of the plugin process. 2. YASARA returns command results back to the plugin via a a local socket server that is launched as part of the initialisation process. [#]_ 3. A *"YASARA command"* always implies a *Yanaconda command*. There are *"no actual Python bindings"*. All of the functions in ``yasara.py`` are very simple adapters that format a string with the equivalent Yanaconda command and pass it back to YASARA. This communication is handled by ``runretval()`` of the ``yasara.py`` module. Every one of the Python functions is structured along the following template: :: def some_function(p1, p2, pn=None): # Build the equivalent Yanaconda command yanaconda_string = f"SomeFunction parameter1={p1}, parameter2={p2}" if pn is not None: yanaconda_string += f" pn={pn}" # Send it to YASARA command_result = runretval(yanaconda_string) # Return the value to the Python code return command_result And here is a (simplified) view of what happens within ``runretval()``: :: def runretval(command): global com # If the Local_Socket_Server has not been initialised yet, initialise it here. if (com==None): com = yasara_communicator() # Use stdout to send the command to YASARA and use the Local_Socket_Server # port p to return the results. sys.stdout.write('ExecRV%d: '%com.port+command+'\n') sys.stdout.flush() # Accept the connection immediately com.accept() return(com.receivemessage(com.RESULT)) At this point, try not to worry too much about "Pythonisms" or optimisations and focus on understanding the round-trip from Python function call to returning any results. When is this not working? ========================= This process works (has worked) sufficiently well as long as ``yasara.py`` is imported by **the same process that launched the plugin**. In that case, the ``stdout`` that ``yasara.py`` is using is the exact same ``stdout`` that the plugin "sees" as well and everything works well. But, what is different when ``yasara.py`` is imported by a process that is **different** than the plugin process? Just as it happens in the case of YaPyCon, the plugin itself launches the Python Console as a separate process [#]_. This creates a complete mismatch in two points: 1. The ``stdout`` stream of the new processes is *entirely unrelated* to the ``stdout`` that YASARA is connected to. * For example, in the case of the Python Console, the ``stdout`` is simply redirected to the console itself. 2. Importing ``yasara.py`` from that separate process, will still go through the initialisation process (*"Initialise plugin variables"*), it will re-discover a completely different port ``p`` (launching yet another ``Local_Socket_Server``) and will attempt to pass that port information back to YASARA. **That** step will fail, because ``stdout`` **is not pointing back to YASARA**. At that point, the whole plugin hangs waiting for a response from the main YASARA program (that is now not even aware that a Yanaconda command was sent to it). These two conditions render any subsequent use of ``import yasara`` from other processes entirely useless [#]_. .. thumbnail:: resources/figures/fig_mermaid_when_comms_break.png .. .. mermaid:: :caption: Simplified sequence diagram of importing ``yasara`` from a "secondary" process. sequenceDiagram autonumber participant YASARA participant Plugin participant yasara.py_1 participant Local_Socket_Server_1 participant Python_Console participant yasara.py_2 participant Local_Socket_Server_2 YASARA ->>Plugin: Launch plugin with
request (r), listen on
stdout for
Yanaconda commands. Plugin ->>yasara.py_1: import yasara yasara.py_1 ->>yasara.py_1: Initialise plugin variables yasara.py_1 ->>yasara.py_1: Discover a free port (p) yasara.py_1 ->>Local_Socket_Server_1: Launch on port p
yasara.py_1 ->>YASARA: Pass p back to YASARA
(as part of calling ``LoadStorage``) yasara.py_1 ->>Plugin: Import complete,
return to plugin
code execution Plugin->>Plugin: Examine (r) Plugin->>Python_Console: Launch console Python_Console->>yasara.py_2: import yasara yasara.py_2 ->>yasara.py_2: Initialise plugin variables yasara.py_2 ->>yasara.py_2: Discover a free port (p2) yasara.py_2 ->>Local_Socket_Server_2: Launch on port p2
rect rgb(232,88,88) yasara.py_2 --xYASARA: Pass p2 back to ... end In this sequence, that last step is getting lost in the "pipework". Remarks ------- 1. There is no need to launch a new local server because the plugin has already started one. That is not too problematic in itself, after all, YASARA only needs to know which server to send its response to. 2. The connection to the processes' ``stdout`` has been lost. Therefore, the ``runretval()`` of ``yasara.py`` *as imported from the console process* cannot communicate with the original YASARA process. Adding Remote Procedure Calling =============================== The solution to this situation was to find a way to "propagate" the already initialised variables, from ``yasara.py`` to any subsequent processes. And this way was Remote Procedure Calling (RPC), via the `rpyc `_ module. ``rpyc`` provides a convenient mechanism for a Python program to call functions or access memory as if it was part of the context of one process but in actual fact these residing elsewhere (in a different process or even different computer). This solution is similar to launching yet another socket server but acting in an almost transparent way to coordinate calls across the network. Adding the ``yasara_kernel.py`` module ====================================== In a typical YASARA plugin, ``yasara.py`` must be the first module to be imported by a plugin for it to be able to communicate with the main YASARA program. Similarly, from within the Python console, the first thing to do is to import ``yasara_kernel.py``. The two modules are *almost* identical. However, in developing YaPyCon it was quickly realised that: 1. For backwards compatibility and the stability of YASARA, it was not possible to alter the existing ``yasara.py`` at all. 2. ``yasara.py`` provided access to a number of functions that could work in a "self-destructing" way if launched from within YaPyCon. For example, allowing ``Exit()`` could lead to "zombie" processes where the Python Console could still go on after having sent a command to YASARA to close. 3. Decoupling ``yasara_kernel.py`` from ``yasara.py`` provided an additional flexibility to modify the structure of the module without worrying about the effect of these modifications to existing plugins. * Part of these modifications was to add functions for "unpacking" certain result types as returned by YASARA, for convenience. In any case, such "unpacking" is expected to commonly occur in a given plugin, apart from trivial cases. How YaPyCon works ================= Having described all this, a simplified view of the main actors in the communications between the YASARA Python Console and the main process of YASARA now looks like this: .. thumbnail:: resources/figures/fig_mermaid_yapycon_operation.png .. .. mermaid:: :caption: Simplified sequence diagram of the most important actors in the communications between the Python Console and YASARA. sequenceDiagram autonumber participant YASARA participant YaPyCon_Plugin participant yasara.py participant Local_Socket_Server participant Python_Console participant yasara_kernel.py participant RPC_Server YASARA ->>YaPyCon_Plugin: Launch plugin with
request (r), listen on
stdout for
Yanaconda commands. YaPyCon_Plugin ->>yasara.py: import yasara yasara.py ->>yasara.py: Initialise plugin variables yasara.py ->>yasara.py: Discover a free port (p) yasara.py ->>Local_Socket_Server: Launch on port p
yasara.py ->>YASARA: Pass p back to YASARA
(as part of calling ``LoadStorage``) yasara.py ->>YaPyCon_Plugin: Import complete,
return to plugin
code execution YaPyCon_Plugin->>YaPyCon_Plugin: Examine (r) YaPyCon_Plugin->>RPC_Server: Launch server at 18861 YaPyCon_Plugin->>Python_Console: Launch console Python_Console->>yasara_kernel.py: import yasara_kernel yasara_kernel.py->>RPC_Server: Connect yasara_kernel.py->>RPC_Server: Get proxy
objects from yasara.py RPC_Server->>YaPyCon_Plugin: Get proxy objects YaPyCon_Plugin->>RPC_Server: Return proxy objects RPC_Server->>yasara_kernel.py:Return proxy objects yasara_kernel.py->>yasara_kernel.py: Initialise local
plugin variables yasara_kernel.py->>Python_Console:Continue execution Python_Console->>Python_Console:Enter Read-Eval-Print Loop For more details about each of the points mentioned in this section, please see :ref:`api` ----- .. [#] This process is simplified here for economy of space. More accurately, the discovery of a free port and the socket server binding are handled by class ``yasara_communicator`` that is "constructed" as part of the ``LoadStorage()`` command. The latter is called as part of the ``yasara.py`` initialisation of variables. .. [#] Again, this is a simplification for economy of space and scope. In actual fact, the Python console is launched as a set of processes, threads and communication channels because of the way the Jupyter protocol operates. A full description of that would be out of the scope of this document but mich more information is available at the `Messaging in Jupyter section `_ in the main ``jupyter_client`` documentation. .. [#] This is not unknown to the YASARA developers. In fact, ``yasara.py`` includes a workaround that allows one to take control of YASARA from a browser. This is solved via launching yet another ``Local_Socket_Server`` and more information is available at :ref:`plugin_plumbing`