SEMOSS facilitates interoperability between its Java backend and Python scripts/environments. This communication is primarily achieved through a TCP-based client-server architecture, allowing Java to invoke Python functions, execute scripts, and exchange data.
flowchart TD
Start([Java Component Needs Python]) --> Init[Initialize Python Server]
Init --> InitSteps[ClientProcessWrapper:<br/>- Allocate Port<br/>- Launch gaas_tcp_socket_server.py<br/>- Create SocketClient Connection]
InitSteps --> Ready{Server Ready?}
Ready -->|Yes| WaitRequest[Python Server Listening<br/>ThreadingTCPServer]
Ready -->|No| Error1[Error: Connection Failed]
WaitRequest --> JavaRequest[Java Sends Request<br/>PyTranslator.runScript]
JavaRequest --> BuildPayload[Build PayloadStruct:<br/>- operation: PYTHON<br/>- script content<br/>- insightId]
BuildPayload --> SendTCP[Send via TCP Socket<br/>Format: size + epoc + JSON]
SendTCP --> PythonReceive[TCPServerHandler Receives]
PythonReceive --> ParsePayload[Deserialize JSON Payload]
ParsePayload --> CheckOp{Operation Type?}
CheckOp -->|PYTHON| ExecPython[Execute Python Code<br/>eval/exec with SemossConsole]
CheckOp -->|REACTOR/ENGINE| Callback[Handle Callback from Python]
CheckOp -->|Other| OtherOp[Handle Other Operations]
ExecPython --> StreamOutput[Stream stdout/stderr<br/>interim=true messages]
StreamOutput --> PythonNeedsJava{Python Needs<br/>Java Resource?}
PythonNeedsJava -->|Yes| PyCallback[ServerProxy.callReactor/<br/>callEngine]
PythonNeedsJava -->|No| CaptureResult[Capture Final Result]
PyCallback --> SendCallback[Send Callback Request<br/>to Java with new epoc]
SendCallback --> JavaExec[Java Executes<br/>Reactor/Engine Method]
JavaExec --> RetCallback[Return Callback Result<br/>to Python]
RetCallback --> CaptureResult
CaptureResult --> CheckError{Exception<br/>Occurred?}
CheckError -->|Yes| SendError[Send Response<br/>with ex field set]
CheckError -->|No| SendResult[Send Response<br/>payload with result]
SendError --> JavaReceive[Java SocketClient<br/>Receives Response]
SendResult --> JavaReceive
JavaReceive --> JavaParse[Deserialize PayloadStruct]
JavaParse --> JavaCheck{Check ex field}
JavaCheck -->|Error| ThrowException[Throw SemossPixelException]
JavaCheck -->|Success| ReturnResult[Return ps.payload]
ThrowException --> Complete
ReturnResult --> Complete
Complete --> MoreRequests{More Requests?}
MoreRequests -->|Yes| WaitRequest
MoreRequests -->|No| Shutdown[Shutdown Request]
Shutdown --> Cleanup[ClientProcessWrapper.shutdown:<br/>- Close Socket<br/>- Terminate Python Process]
Cleanup --> End([End])
Error1 --> End
style Start fill:#e1f5e1
style End fill:#ffe1e1
style ExecPython fill:#e3f2fd
style JavaExec fill:#fff3e0
style CheckError fill:#fff9c4
style JavaCheck fill:#fff9c4
style PythonNeedsJava fill:#f3e5f5
The core mechanism involves:
- A Java client component that initiates requests and sends data.
- A Python TCP server that listens for these requests, processes them, executes Python code, and returns results.
- A defined communication protocol (often involving JSON or serialized data) for message exchange.
This setup enables SEMOSS to leverage Python's rich ecosystem of libraries for data science, machine learning, and other specialized tasks, while managing the overall workflow and user interaction from the Java backend.
On the Java side, several classes collaborate to manage the Python process, establish communication, and send commands or data.
- Role: This class is the primary high-level Java client for interacting with the Python TCP server. It provides a simpler API for other Java components (like
PandasFrame, Python-specific reactors, or services needing Python execution) to run Python code and exchange data, abstracting the direct complexities of socket communication and data serialization. - Functionality:
- Script Execution:
runScript(String script): Sends a Python script string directly to the Python server for execution and retrieves the result. This is suitable for short, self-contained scripts or commands.runEmptyPy(String... scriptLines): For more complex or multi-line scripts, this method writes the script lines to a temporary.pyfile (usually within the insight'sPy/Temp/directory). It then instructs the Python server (via a command likesmssutil.run_empty_wrapper(filePath, globals())) to execute this script file. The result is not directly captured from stdout by this method; it's for scripts that perform actions without a direct return value to Java or that write their own output.runPyAndReturnOutput(String... scriptLines): Similar torunEmptyPy, it writes the script to a file. However, it also tells the Python wrapper (smssutil.runwrapper) to redirect Python's output (stdout/stderr) to a temporary text file.PyTranslatorthen reads this file to get the script's output. It also handles replacing absolute insight paths in the output with generic variables like$IF.runSingle(String script, Insight insight): A synchronized method that sets up insight-specific paths (ROOT,APP_ROOT,USER_ROOT) as global variables in the Python environment before executing the provided script (via a temporary file andsmssutil.runwrapper_eval). This is useful when Python scripts need to be aware of the current insight's file system context.
- Data Type Conversion: Provides
convertDataType(String pDataType)to map Python data type strings (e.g., "int64", "float64") to SEMOSS's internalSemossDataTypeenum. - Socket Client Usage: It holds a reference to a
prerna.tcp.client.SocketClientinstance (obtained fromClientProcessWrapper) and uses it to sendPayloadStructobjects (withOPERATION.PYTHON) to the Python server. - Path Management: Manages temporary file creation for scripts and outputs, often within the context of an
Insight's working directory.
- Script Execution:
-
prerna.om.ClientProcessWrapper:- Role: This class is responsible for the lifecycle management of the external Python TCP server process and the Java-side
SocketClientthat connects to it. An instance ofClientProcessWrapperis often associated with a specific Python environment or a long-running Python service required by SEMOSS (e.g., for vector database operations or other persistent Python backends). - Functionality:
createProcessAndClient(...):- Determines an available network port using
prerna.util.PortAllocator. - Launches the Python TCP server as an external process. It can start a "native" Python server (e.g.,
gaas_tcp_socket_server.pyviaprerna.util.Utility.startTCPServerNativePy) or potentially a generic Java-based TCP server ifnativePyServeris false (though native Python is typical for this use case). - Supports starting the Python process within a chroot environment for isolation if a
SymlinkHelperis provided. - Configures logging for the Python server process by writing a
log4j.propertiesfile to its working directory. - Instantiates and connects a
SocketClient(eitherprerna.tcp.client.NativePySocketClientorprerna.tcp.client.SocketClient) to the newly started Python server.
- Determines an available network port using
shutdown(boolean cleanUpFolder): Gracefully stops theSocketClient(sending a shutdown command to the Python server) and terminates the external Python process. It can also optionally delete the temporary server directory.reconnect(): Provides a mechanism to restart the Python server process and re-establish the client connection if it's lost.- Provides access to the managed
SocketClientinstance, whichPyTranslatorthen uses for communication.
- Role: This class is responsible for the lifecycle management of the external Python TCP server process and the Java-side
-
prerna.tcp.client.SocketClient/prerna.tcp.client.NativePySocketClient:- Role: These classes handle the low-level TCP/IP socket communication with the Python server.
- Functionality:
- Establishing and maintaining the socket connection.
- Sending
PayloadStructobjects (which encapsulate commands and data) to the Python server. - Receiving
PayloadStructresponses from the Python server. - Handling I/O streams, message serialization/deserialization (likely of the
PayloadStructitself), and basic error detection on the communication channel. NativePySocketClientmight have specific optimizations or handling for Python communication compared to a genericSocketClient.
In essence, ClientProcessWrapper sets up and tears down the entire Python server environment and the basic communication line, while PyTranslator uses that line to conduct specific conversations (i.e., send scripts and data) with Python.
The Python side consists of a TCP server that listens for requests from Java, a handler to process these requests, and a mechanism for Python to call back into Java if needed.
- Role: This script implements the main Python TCP server. It's responsible for listening for incoming connections from Java clients (like
PyTranslatorviaClientProcessWrapper). - Functionality:
- It uses the standard Python
socketserver.ThreadingTCPServerto handle multiple client connections concurrently, each in its own thread. - It's launched by the Java
ClientProcessWrapper, receiving parameters like port, working directories (py_folder,insight_folder), and timeout settings via command-line arguments. - For each incoming connection, it instantiates
gaas_tcp_server_handler.TCPServerHandlerto manage the communication with that specific Java client. - Includes a timeout mechanism to shut down the server if it's idle (no active connections) for a specified period.
- Supports running in a
chrootenvironment for enhanced security and isolation, based on parameters passed from Java. - Chroot Jail and Security:
- The server can be started within a
chrootjail if theuserChrootFolderparameter is provided by the JavaClientProcessWrapper. - What is
chroot?:chroot(change root) is a Unix/Linux system call that changes the apparent root directory for the current running process and its children. This means the process cannot "see" or access files outside of this designated directory tree, effectively creating a sandboxed environment. - Purpose in SEMOSS: This is primarily a security measure to isolate the Python server process. It limits the Python script's file system access to only the specified chroot directory, preventing it from potentially accessing or modifying sensitive system files outside its intended scope.
prerna.util.SymlinkHelper: WhenClientProcessWrapperstarts the Python server in chroot mode (e.g., viaprerna.util.Utility.startTCPServerNativePyChroot), it often utilizesprerna.util.SymlinkHelper.- The
SymlinkHelperis responsible for creating necessary symbolic links within the chroot jail. These symlinks might point to:- Required Python libraries or packages.
- SEMOSS's own Python utility scripts (like those in the
py/directory that the server needs). - Specific insight or asset folders that the Python script needs to access for its operations.
- Without these symlinks, the chrooted Python process, being confined to its new root, wouldn't be able to find and load these essential dependencies.
- The
- Docker Context: While Docker itself provides containerization (a stronger form of isolation), if SEMOSS is run within a Docker container, the
chrootmechanism for the Python server can provide an additional layer of defense-in-depth, especially if the Python server handles code or data from potentially less trusted sources or if multiple users/tenants might indirectly cause Python scripts to execute. The Dockerfile might set up a base file system, andchrootfurther restricts specific Python server instances within that. - The
gaas_tcp_socket_server.pyscript itself performsos.chroot(args.userChrootFolder)andos.chdir("/")if theuserChrootFolderargument is provided.
- The server can be started within a
- It uses the standard Python
- Role: The
TCPServerHandlerclass is the core request processing unit on the Python side. An instance is created for each connected Java client. - Functionality:
- Message Handling:
- In its
handle()method, it continuously listens for messages from the Java client. - It reads messages prefixed with their size (4 bytes) and an "epoc" ID (20 bytes, a unique request identifier generated by Java).
- Deserializes the message payload (typically JSON) into a Python dictionary. This dictionary structure mirrors the Java
PayloadStruct.
- In its
- Python Execution (
handle_python):- If the received payload's
operationis "PYTHON", it extracts the Python script/command. - It uses a
semoss_console.SemossConsoleinstance to capturestdoutandstderrfrom the executed Python code. This console streams captured output back to Java as "interim" messages. - It executes the Python command using
eval()orexec(). It includes a customexecute_and_capturemethod to try and return the value of the last expression in a script, similar to a Jupyter notebook cell. - The final result of the execution (or any exception traceback) is then sent back to Java as a "response" message.
- If the received payload's
- Response Handling (
handle_response): If the incoming message from Java is a response to a request initiated by Python (viagaas_server_proxy.py), this method is triggered. It uses athreading.Conditionobject (stored in a sharedmonitorsdictionary, keyed by the originalepoc) to wake up the Python thread that made the callback to Java and deliver the response. - Output Formatting (
send_output): Packages Python results (or errors) into a JSON payload (again, mirroringPayloadStruct), prefixes it with size and the originalepoc, and sends it back over the socket to the Java client. - Shell Command Execution (
handle_shell): Contains limited, experimental functionality to execute shell commands likecd,ls,gitwithin a sandboxed environment related to theinsight_folder. This is not the primary purpose of the server.
- Message Handling:
- Role: This script provides the
ServerProxyclass, which enables Python code running within theTCPServerHandlerto make calls back to the Java backend. This is crucial for scenarios where Python needs to leverage Java-side functionalities (e.g., query a SEMOSS engine, execute a Pixel script). - Functionality:
comm(...)method:- Constructs a request payload (dictionary) similar to the one Java sends to Python. This payload includes an
epoc(a new unique ID for this Python-to-Java request),engineType,engineId,methodName, arguments (payload), argument types (payloadClassNames),insightId, and anoperation(e.g., "REACTOR" or "ENGINE"). - It registers a
threading.Conditionin theTCPServerHandler'smonitorsdictionary, associated with theepocof this outgoing request. - It then uses the
TCPServerHandler'ssend_request()method to send this payload to the connected Java client. - The Python thread then calls
wait()on theCondition, pausing until Java sends back a response with the sameepoc.
- Constructs a request payload (dictionary) similar to the one Java sends to Python. This payload includes an
callReactor(...)andcallEngine(...): These are higher-level methods that simplify making specific types of calls (executing a Pixel reactor or an IEngine method) to Java by wrapping thecomm()method. They manage the thread creation and waiting for the response.
The communication between Java and Python relies on a TCP socket connection and a JSON-based message protocol.
-
Message Structure:
- Each message (in both directions) is prefixed with:
- Size (4 bytes): An integer indicating the length of the subsequent JSON payload, sent in big-endian byte order.
- Epoc (20 bytes): A string representing a unique identifier for the request. For responses, this
epocmatches theepocof the original request.
- The main part of the message is a JSON string, which, when deserialized, typically corresponds to the structure of Java's
prerna.tcp.PayloadStruct. This structure generally includes:epoc: The unique request/response identifier.payload: A list containing the actual data or commands. For example, for a Python command,payload[0]would be the script string. For results, it would contain the Python output.operation: A string indicating the type of operation (e.g., "PYTHON", "CMD", "REACTOR", "ENGINE", "STDOUT").response: A boolean flag, true if the message is a response to a previous request.interim: A boolean flag, true if the message is a partial/streamed output (like stdout from Python).ex: Contains error/exception details if an error occurred.- Other fields for specific operations (e.g.,
insightId,methodName,engineIdfor callbacks from Python to Java).
- Each message (in both directions) is prefixed with:
-
Data Serialization:
- Java to Python:
PyTranslatorserializes thePayloadStructinto a JSON string.- Data intended for Python (e.g., for creating Pandas DataFrames) is often passed as strings within the script itself or by instructing Python to read from files prepared by Java.
- Python to Java:
TCPServerHandlerusesjson.dumps(orjsonpickle.encode) to serialize Python objects (dictionaries, lists, primitive types, or custom objects with appropriate handlers) into a JSON string for thepayloadfield of the response message.- Pandas DataFrames might be converted to a dictionary format (e.g.,
to_dict(orient="split")) before JSON serialization. Special handling forNaNvalues (converted to "NaN" string) anddatetime64(converted to string) exists.
- Java to Python:
-
Error Handling:
- Exceptions occurring during Python script execution are caught by
TCPServerHandler. The traceback is converted to a string and placed in theexfield of the response payload sent back to Java. - Java's
PyTranslatorchecks theexfield in the receivedPayloadStructand throws aSemossPixelExceptionif an error is present.
- Exceptions occurring during Python script execution are caught by
-
Streaming Output (Stdout/Stderr):
- The
semoss_console.SemossConsoleclass in Python capturesstdoutandstderr. - It sends this captured output back to Java via
TCPServerHandler.send_output()withoperation="STDOUT"andinterim=True. - The Java side (
SocketClientor a listener) receives these interim messages and can process them (e.g., log them or display them in a console). A final "D.O.N.E" marker in the stream often indicates the end of stdout/stderr for a command.
- The
This bidirectional JSON-over-TCP protocol allows SEMOSS to integrate Java and Python execution environments effectively, enabling complex workflows that leverage the strengths of both languages.