doc and various fixes of lua for windows

This commit is contained in:
2026-01-03 19:29:14 +01:00
parent 487156785a
commit 7954f2cb2b
9 changed files with 283 additions and 63 deletions

View File

@@ -17,7 +17,7 @@ python_path_Linux: $(home)/tmp/tum_venv/bin/python3
LUA_PATH_Linux: /usr/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;/usr/lib/lua/5.4/?.lua;/usr/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua;/home/francois/.luarocks/share/lua/5.4/?.lua;/home/francois/.luarocks/share/lua/5.4/?/init.lua LUA_PATH_Linux: /usr/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;/usr/lib/lua/5.4/?.lua;/usr/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua;/home/francois/.luarocks/share/lua/5.4/?.lua;/home/francois/.luarocks/share/lua/5.4/?/init.lua
LUA_CPATH_Linux: /usr/local/lib/lua/5.4/?.so;/usr/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so;/usr/lib/lua/5.4/loadall.so;./?.so;/home/francois/.luarocks/lib/lua/5.4/?.so LUA_CPATH_Linux: /usr/local/lib/lua/5.4/?.so;/usr/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so;/usr/lib/lua/5.4/loadall.so;./?.so;/home/francois/.luarocks/lib/lua/5.4/?.so
PATH_Linux: "" PATH_Linux:
LUA_PATH_Windows: ;.\?.lua;C:\Lua\5.1\lua\?.lua;C:\Lua\5.1\lua\?\init.lua;C:\Lua\5.1\?.lua;C:\Lua\5.1\?\init.lua;C:\Lua\5.1\lua\?.luac LUA_PATH_Windows: ;.\?.lua;C:\Lua\5.1\lua\?.lua;C:\Lua\5.1\lua\?\init.lua;C:\Lua\5.1\?.lua;C:\Lua\5.1\?\init.lua;C:\Lua\5.1\lua\?.luac
LUA_CPATH_Windows: .\?.dll;C:\Lua\5.1\?.dll;C:\Lua\5.1\loadall.dll;C:\Lua\5.1\clibs\?.dll;C:\Lua\5.1\clibs\loadall.dll;.\?51.dll;C:\Lua\5.1\?51.dll;C:\Lua\5.1\clibs\?51.dll LUA_CPATH_Windows: .\?.dll;C:\Lua\5.1\?.dll;C:\Lua\5.1\loadall.dll;C:\Lua\5.1\clibs\?.dll;C:\Lua\5.1\clibs\loadall.dll;.\?51.dll;C:\Lua\5.1\?51.dll;C:\Lua\5.1\clibs\?51.dll

View File

@@ -13,7 +13,7 @@ Tests reports generation and customization are also in this tool's scope.
Its main features are: Its main features are:
* YAML test description, * YAML test description,
* Test configuration files in YAML, JSON or XML, * Test configuration files in YAML,
* Full range of pre-existing Test items, * Full range of pre-existing Test items,
* Test steps, loops, * Test steps, loops,
* Dynamic variables expansion at test runtime, * Dynamic variables expansion at test runtime,

View File

@@ -63,3 +63,24 @@ on how to access to global variables from test items and scripts).
In the example above, the global variable ``$(lfn_activity)`` In the example above, the global variable ``$(lfn_activity)``
would be created at the end of the item execution. It would contain the resulting would be created at the end of the item execution. It would contain the resulting
value of the funcToBeExecuted python function. value of the funcToBeExecuted python function.
**Global variables**
Some global variables have an impact on the ``lua_func`` test item behavior:
* ``lua_path``: This optional global variable can be used to define
the lua executable path. If not defined, the lua interpreter is
searched in at the default place in the system.
* ``lua_env``: This global variable can be used to define
environment variables for the lua script execution environment.
Only `PATH`, `LUA_PATH`, and `LUA_CPATH` are supported.
.. code-block:: yaml
:caption: example of configuration file: param.yaml
[...]
lua_env:
PATH: "/my/path/"
LUA_PATH: "/my/lua/modules/?.lua;;"
LUA_CPATH: "/my/lua/modules/?.so;;"
[...]

View File

@@ -106,3 +106,11 @@ on how to access to global variables from test items and scripts).
In the example above, the global variable ``$(pfn_function test item)`` In the example above, the global variable ``$(pfn_function test item)``
would be created at the end of the item execution. It would contain the resulting would be created at the end of the item execution. It would contain the resulting
value of the funcToBeExecuted python function. value of the funcToBeExecuted python function.
**Global variables**
Some global variables have an impact on the ``py_func`` test item behavior:
* ``python_path``: This optional global variable can be used to define
the python executable path. If not defined, the python interpreter is
searched in at the default places in the system.

View File

@@ -36,60 +36,52 @@ The example below shows a basic implementation of the TUM description file:
Configuration files Configuration files
-------------------- --------------------
A configuration file can be specified in the .tum file or by the command line. A configuration file can be specified in the `.tum` file or by the command line.
This configuration file is optional. This configuration file is optional and must be a YAML file.
It can be of three different syntax: The type of file is recognized by the file name extension `.yaml`.
* XML
* YAML
* JSON
The type of file is recognized by the file name extension (.xml, .yaml, .json).
During the test script loading process, the values defined in these configuration files During the test script loading process, the values defined in these configuration files
are added to the global variables and are then accessible from the test items and scripts are added to the global variables and are then accessible from the test items and scripts
(cf. :ref:`global variables<sec_global_variables>`). (cf. :ref:`global variables<sec_global_variables>`).
The parameter file can be specified in the .tum file root: The parameter file can be specified in the `.tum` file root:
.. code-block:: yaml .. code-block:: yaml
:caption: configuration files definition :caption: configuration files definition in the main `.tum` test file
config_file: config_file:
- myparam.xml config1.yaml
- config1.json config2.yaml
- config2.yaml
main: main:
name: Test example name: Test example
[...] [...]
If nothing is specified, the ``param.xml``, ``param.yaml`` and ``param.json`` .. code-block:: yaml
are automatically loaded, if present in the test directory. :caption: example of configuration file: param.yaml
parameter1: value1
parameter2: 1234
parameter3: <@ 12.34 * 2 @>
parameter4:
- $(parameter1)
- $(parameter3)
parameter5:
sub_param1: sub_value1
sub_param2: $(parameter4)
If nothing is specified, the ``param.yaml``
is automatically loaded, if present in the test directory.
Files loading Files loading
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
The ``JSON`` and ``YAML`` configuration files variables are evaluated directly. The ``YAML`` configuration files variables are evaluated directly and accessible from TUM
tests description files and also from :ref:`python<sec_py_func_item>`
and :ref:`lua<sec_py_func_item>` function test items.
The XML files content is evaluated as follows. See more details :ref:`below<sec_global_variables>`.
.. code-block:: xml
:name: param.xml
<?xml version="1.0" ?>
<root>
<parameter name="param1" value="['abc', 'bcd']"/>
<parameter name="param2" value="0x123454"/>
<parameter name="param3" str="def"/>
</root>
If the ``parameter`` XML item defines:
* ``value`` argument: its content is parsed for variable substitution
(see :ref:`variables expansion<sec_variable_expansion>`) and then evaluated as a python statement,
* ``str`` argument: its content is not evaluated and is kept as a string.
.. _sec_global_variables: .. _sec_global_variables:
@@ -153,11 +145,32 @@ library API (see :ref:`helper library<sec_python_helper_library>`)
:ref:`sec_loop_item`). If the loop number its value is the python constant :ref:`sec_loop_item`). If the loop number its value is the python constant
``inf``. ``inf``.
Debug mode
^^^^^^^^^^^^^^^^^^^
Debug mode can be enabled by defining the global variable `test_debug`.
For example, it can be defined in the configuration file as:
.. code-block:: yaml
:caption: example of configuration file: param.yaml
[...]
test_debug: True
[.]
It can also be defined from the command line with the option
``-d test_debug``.
When debug mode is enabled, additional information are displayed in the log window.
Some :ref:`helper library functions<sec_python_helper_library>` are availabe
to give the state of the debug mode.
Test items entries Test items entries
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
All test items attributes can be global variable entry; All test items attributes can be global variable entries;
when using the entry ``$(<global>)`` before a key value, the corresponding when using the entry ``$(<global>)`` before a key value, the corresponding
key entry is searched within the global variables dataset. key entry is searched within the global variables dataset.

View File

@@ -5,6 +5,7 @@ import threading
import itertools import itertools
from time import sleep from time import sleep
from typing import Callable, Any from typing import Callable, Any
import libs.testium as tm
from interpreter.utils.tum_except import ETUMRuntimeError from interpreter.utils.tum_except import ETUMRuntimeError
@@ -53,8 +54,14 @@ Notes:
class JsonRpcConnection: class JsonRpcConnection:
def __init__(self, name, conn: socket.socket, req_handler: Callable[..., Any], timeout=0.2, dbg_out=None): def __init__(
self,
name,
conn: socket.socket,
req_handler: Callable[..., Any],
timeout=0.2,
dbg_out=None,
):
self.name = name self.name = name
self.conn = conn self.conn = conn
if not callable(req_handler): if not callable(req_handler):
@@ -120,9 +127,9 @@ class JsonRpcConnection:
def _dispatch(self, msg): def _dispatch(self, msg):
if "method" in msg: if "method" in msg:
# request to be sent # request to be sent
meth=msg["method"] meth = msg["method"]
params=msg.get("params", None) params = msg.get("params", None)
rid=msg.get("id", None) rid = msg.get("id", None)
threading.Thread( threading.Thread(
target=self._handle_request, args=(meth, params, rid), daemon=True target=self._handle_request, args=(meth, params, rid), daemon=True
@@ -168,10 +175,9 @@ class JsonRpcConnection:
The send operation is protected by a lock to avoid interleaving when The send operation is protected by a lock to avoid interleaving when
multiple threads attempt to write to the underlying socket. multiple threads attempt to write to the underlying socket.
""" """
msg = json.dumps(obj) + "\n" msg = json.dumps(obj) + "\n"
data = (msg).encode() data = (msg).encode()
self.print_info("sending : " + msg) self.print_info(f"sending : " + msg)
with self.send_lock: with self.send_lock:
self.conn.sendall(data) self.conn.sendall(data)
@@ -217,6 +223,7 @@ class JsonRpcConnection:
def join(self): def join(self):
self.recv_thread.join() self.recv_thread.join()
class JsonRpcBase(threading.Thread): class JsonRpcBase(threading.Thread):
"""Threaded base class for simple JSON-RPC server/client helpers. """Threaded base class for simple JSON-RPC server/client helpers.
@@ -236,7 +243,14 @@ class JsonRpcBase(threading.Thread):
- `call()` raises `ETUMRuntimeError` if no active connection exists. - `call()` raises `ETUMRuntimeError` if no active connection exists.
""" """
def __init__(self, host, port, req_handler: Callable[[dict], Any]=None, timeout=10, dbg_out=None): def __init__(
self,
host,
port,
req_handler: Callable[[dict], Any] = None,
timeout=10,
dbg_out=None,
):
super().__init__() super().__init__()
self._host = host self._host = host
self._port = port self._port = port
@@ -276,7 +290,9 @@ class JsonRpcBase(threading.Thread):
self._rpc.stop() self._rpc.stop()
def connect(self, sock): def connect(self, sock):
self._rpc = JsonRpcConnection(self.name, sock, self.handle_request, dbg_out=self.dbg_out) self._rpc = JsonRpcConnection(
self.name, sock, self.handle_request, dbg_out=self.dbg_out
)
self._event_ready.set() self._event_ready.set()
def wait_ready(self, timeout=None): def wait_ready(self, timeout=None):
@@ -292,6 +308,7 @@ class JsonRpcBase(threading.Thread):
if self._rpc is not None: if self._rpc is not None:
self._rpc.dbg_out = dbg_out self._rpc.dbg_out = dbg_out
class JsonRpcSrv(JsonRpcBase): class JsonRpcSrv(JsonRpcBase):
"""Single-connection JSON-RPC server. """Single-connection JSON-RPC server.
@@ -307,7 +324,7 @@ class JsonRpcSrv(JsonRpcBase):
The server will raise `ETUMRuntimeError` on accept/connect timeout. The server will raise `ETUMRuntimeError` on accept/connect timeout.
""" """
def __init__(self, host, port, req_handler = None, timeout=10): def __init__(self, host, port, req_handler=None, timeout=10):
super().__init__(host, port, req_handler, timeout) super().__init__(host, port, req_handler, timeout)
self.name = f"JsonRpcSvr_{port}" self.name = f"JsonRpcSvr_{port}"
@@ -332,7 +349,6 @@ class JsonRpcSrv(JsonRpcBase):
while True: while True:
try: try:
conn, addr = sock.accept() conn, addr = sock.accept()
self.print_info("Client connected")
break break
except socket.timeout: except socket.timeout:
t -= tslice t -= tslice
@@ -369,11 +385,58 @@ class JsonRpcClient(JsonRpcBase):
resp = clt.call('method', {'a': 1}) resp = clt.call('method', {'a': 1})
""" """
def __init__(self, host, port, req_handler = None, timeout=10): def __init__(self, host, port, req_handler=None, timeout=10):
super().__init__(host, port, req_handler, timeout) super().__init__(host, port, req_handler, timeout)
self.name = f"JsonRpcClt_{port}" self.name = f"JsonRpcClt_{port}"
def run(self): def run(self):
if tm.OS() == "Windows":
self.run_win()
else:
self.run_lin()
def run_win(self):
# TCP/IP socket creation
tslice = 1
t = self._timeout
sock = None
try:
while t >= 0:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(tslice)
# Link of the socket at the configured port
try:
sock.connect((self._host, self._port))
break
except socket.timeout:
sock.close()
t -= tslice
if t < 0:
raise ETUMRuntimeError(
f"{self.name}: failed to connect : timeout"
)
else:
sleep(tslice)
except socket.error as e:
raise ETUMRuntimeError(f"{self.name}: failed to connect : {e}")
self.print_info("Connected to server")
self.connect(sock)
while self._rpc.running:
# Sleep a short time to avoid a busy loop and allow
# the receiver thread to process messages.
sleep(0.1)
finally:
if sock is not None:
sock.close()
if self._rpc is not None:
self._rpc.stop()
self._rpc.join()
self.print_info("closed")
def run_lin(self):
# TCP/IP socket creation # TCP/IP socket creation
try: try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
@@ -385,10 +448,12 @@ class JsonRpcClient(JsonRpcBase):
try: try:
sock.connect((self._host, self._port)) sock.connect((self._host, self._port))
break break
except OSError as e: except Exception as e:
t -= tslice t -= tslice
if t < 0: if t < 0:
raise ETUMRuntimeError(f"{self.name}: failed to connect : {e}") raise ETUMRuntimeError(
f"{self.name}: failed to connect : {e}"
)
else: else:
sleep(tslice) sleep(tslice)

View File

@@ -1,4 +1,5 @@
import os import os
import sys
import shutil import shutil
import subprocess import subprocess
import socket import socket
@@ -12,12 +13,36 @@ function_call_process = None
def lua_func_call_init(lua_path, request_handler, timeout): def lua_func_call_init(lua_path, request_handler, timeout):
"""
Initializes the global Lua function execution process.
Args:
lua_path (str): Path to the Lua interpreter executable. If empty, uses system default.
request_handler: Handler for JSON-RPC requests.
timeout (int): Timeout for operations in seconds.
Returns:
LuaFuncExecEngine: The initialized engine instance.
Raises:
ETUMRuntimeError: If the Lua path is invalid or no interpreter is found.
"""
global function_call_process global function_call_process
function_call_process = LuaFuncExecEngine(lua_path, request_handler, timeout) function_call_process = LuaFuncExecEngine(lua_path, request_handler, timeout)
return function_call_process return function_call_process
def is_lua_interpreter(path: str, timeout=2) -> bool: def is_lua_interpreter(path: str, timeout=2) -> bool:
"""
Checks if the given path points to a valid Lua interpreter.
Args:
path (str): Path to the executable to check.
timeout (int, optional): Timeout for the subprocess in seconds. Defaults to 2.
Returns:
bool: True if the path is a Lua interpreter, False otherwise.
"""
try: try:
result = subprocess.run( result = subprocess.run(
[path, "-v"], [path, "-v"],
@@ -32,8 +57,25 @@ def is_lua_interpreter(path: str, timeout=2) -> bool:
class LuaFuncExecEngine: class LuaFuncExecEngine:
"""
Engine for executing Lua functions via a subprocess and JSON-RPC communication.
This class manages a Lua interpreter subprocess, handles RPC communication,
and executes specified functions with parameters.
"""
def __init__(self, lua_path="", request_handler=None, timeout=10): def __init__(self, lua_path="", request_handler=None, timeout=10):
"""
Initializes the Lua function execution engine.
Args:
lua_path (str, optional): Path to the Lua interpreter. Defaults to system path.
request_handler: Handler for JSON-RPC requests.
timeout (int, optional): Timeout for operations in seconds. Defaults to 10.
Raises:
ETUMRuntimeError: If the Lua path is invalid or no interpreter is found.
"""
if lua_path != "": if lua_path != "":
if shutil.which(lua_path) is None: if shutil.which(lua_path) is None:
raise ETUMRuntimeError( raise ETUMRuntimeError(
@@ -61,27 +103,49 @@ class LuaFuncExecEngine:
def start(self): def start(self):
""" """
run the subprocess to execute the python functions of the test. Starts the Lua subprocess for function execution.
Sets up environment variables, binds a socket for communication,
and initializes the JSON-RPC client.
Raises:
ETUMRuntimeError: If the subprocess is already started.
""" """
# This thread is not closed until new test is loaded # This thread is not closed until new test is loaded
if self._process is not None: if self._process is not None:
raise ETUMRuntimeError("The function subprocess has already been started.") raise ETUMRuntimeError("The function subprocess has already been started.")
func_proc_path = os.path.join(tm.gd("testium_path"),"lua_func")
# POpen config
CUST_ENV = {
"PATH": {"replace": False},
"LUA_PATH": {"replace": True},
"LUA_CPATH": {"replace": True},
}
lua_env = tm.gd("lua_env", {})
env = os.environ.copy()
for k, v in CUST_ENV.items():
e = lua_env.get(k, "")
if e != "":
if v["replace"]:
env[k] = e
else:
env[k] = e + ";" + env.get(k, "")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("localhost", 0)) sock.bind(("localhost", 0))
self._port = sock.getsockname()[1] self._port = sock.getsockname()[1]
func_proc_path = os.path.join(tm.gd("testium_path"),"lua_func") # POpen params
lua_env = tm.gd("lua_env", {})
tm.print_debug(f"lua_env : {lua_env}")
params = [self._lpath, "main.lua", "--timeout", f"{self._timeout}", "--host", "127.0.0.1", "--port", f"{self._port}"] params = [self._lpath, "main.lua", "--timeout", f"{self._timeout}", "--host", "127.0.0.1", "--port", f"{self._port}"]
if tm.debug_enabled(): if tm.debug_enabled() and tm.gd("debug_rpc", False):
params.append("--verbose") params.append("--verbose")
self._process = subprocess.Popen( self._process = subprocess.Popen(
params, env=lua_env, cwd=func_proc_path params, env=env, cwd=func_proc_path
) )
# Port was reserved until the sub-process is started. Now released. # Port was reserved until the sub-process is started. Now released.
@@ -89,24 +153,57 @@ class LuaFuncExecEngine:
sock.close() sock.close()
self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler) self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler)
if tm.debug_enabled():
self._rpc.dbg_out = sys.stdout
self._rpc.start() self._rpc.start()
def join(self): def join(self):
"""
Joins the RPC thread and resets the process state.
"""
if self._rpc is not None: if self._rpc is not None:
self._rpc.join() self._rpc.join()
self._rpc = None self._rpc = None
self._process = None self._process = None
def wait_ready(self, timeout=None): def wait_ready(self, timeout=None):
"""
Waits for the RPC client to be ready.
Args:
timeout (float, optional): Timeout in seconds. Defaults to None.
Returns:
bool: True if ready, False otherwise.
"""
if self._rpc is not None and self._rpc.is_alive(): if self._rpc is not None and self._rpc.is_alive():
return self._rpc.wait_ready(timeout) return self._rpc.wait_ready(timeout)
return False return False
def stop(self): def stop(self):
"""
Stops the RPC client.
"""
if self._rpc is not None: if self._rpc is not None:
self._rpc.stop() self._rpc.stop()
def func_call(self, file: str, func_name: str, params: list, verbose: bool = True): def func_call(self, file: str, func_name: str, params: list, verbose: bool = True):
"""
Calls a Lua function via RPC and returns the result.
Args:
file (str): Path to the Lua file containing the function.
func_name (str): Name of the function to call.
params (list): List of parameters to pass to the function.
verbose (bool, optional): Whether to enable verbose output. Defaults to True.
Returns:
tuple: (TestValue.SUCCESS, (result, reported_values)) on success,
(TestValue.FAILURE, error_message) on failure.
Raises:
ETUMRuntimeError: If the RPC call fails or no process is active.
"""
if (self._rpc is not None) and self._rpc.is_alive(): if (self._rpc is not None) and self._rpc.is_alive():
answer = self._rpc.call( answer = self._rpc.call(
"func_call", "func_call",
@@ -143,7 +240,21 @@ class LuaFuncExecEngine:
def lua_func_exec(file: str, func_name: str, params: list, verbose: bool = True): def lua_func_exec(file: str, func_name: str, params: list, verbose: bool = True):
"""Executes a python function and returns its result and reported values""" """
Executes a Lua function using the global function call process.
Args:
file (str): Path to the Lua file containing the function.
func_name (str): Name of the function to call.
params (list): List of parameters to pass to the function.
verbose (bool, optional): Whether to enable verbose output. Defaults to True.
Returns:
tuple: (success_status, result_or_error) where success_status is TestValue.SUCCESS or FAILURE.
Raises:
ETUMRuntimeError: If no function execution process is active.
"""
global function_call_process global function_call_process
if function_call_process is not None: if function_call_process is not None:

View File

@@ -119,7 +119,6 @@ def _sys_app_path_win(app_name):
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired): except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
data = "" data = ""
sys_python_path = data.splitlines() sys_python_path = data.splitlines()
tm.print_debug("data = ", data)
for l in sys_python_path: for l in sys_python_path:
if f"{app_name}.exe" in l: if f"{app_name}.exe" in l:
return l return l

View File

@@ -76,7 +76,8 @@ class PyFuncExecEngine:
func_proc_path = tm.gd("testium_path") func_proc_path = tm.gd("testium_path")
params = [self._ppath, "-m", "py_func", "-p", f"{self._port}", "-t", f"{self._timeout}"] params = [self._ppath, "-m", "py_func", "-p", f"{self._port}", "-t", f"{self._timeout}"]
if tm.debug_enabled():
if tm.debug_enabled() and tm.gd("debug_rpc", False):
params.append("-v") params.append("-v")
self._process = subprocess.Popen( self._process = subprocess.Popen(
@@ -88,6 +89,8 @@ class PyFuncExecEngine:
sock.close() sock.close()
self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler) self._rpc = JsonRpcClient("localhost", self._port, req_handler=self._req_handler)
if tm.debug_enabled():
self._rpc.dbg_out = sys.stdout
self._rpc.start() self._rpc.start()
def join(self): def join(self):