diff --git a/.vscode/launch.json b/.vscode/launch.json index df34bc8443..3db609408c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -379,9 +379,10 @@ "program": "${workspaceFolder}/out/linux/x64/ten_manager/bin/tman", "cwd": "${workspaceFolder}/out/linux/x64/", "args": [ + "--verbose", "dev-server", - "--base-dir=/home/wei/MyData/Temp/ASTRA.ai/agents", - "--port=49484" + "--base-dir=/home/sunxilin/ten_framework_internal_base/ten_framework/out/linux/x64/tests/ten_runtime/integration/python/two_async_extensions_in_different_groups_python/two_async_extensions_in_different_groups_python_app", + "--port=49483" ], }, { diff --git a/core/include_internal/ten_runtime/binding/python/ten_env/ten_env.h b/core/include_internal/ten_runtime/binding/python/ten_env/ten_env.h index cdd0c7acea..8e45716db4 100644 --- a/core/include_internal/ten_runtime/binding/python/ten_env/ten_env.h +++ b/core/include_internal/ten_runtime/binding/python/ten_env/ten_env.h @@ -26,6 +26,7 @@ typedef struct ten_py_ten_env_t { // Mark whether the gil state need to be released after 'on_deinit_done'. bool need_to_release_gil_state; + PyThreadState* py_thread_state; } ten_py_ten_env_t; TEN_RUNTIME_PRIVATE_API bool ten_py_ten_env_check_integrity( diff --git a/core/src/ten_runtime/binding/python/interface/ten/__init__.py b/core/src/ten_runtime/binding/python/interface/ten/__init__.py index cab4f2beb6..ca3eb43b8c 100644 --- a/core/src/ten_runtime/binding/python/interface/ten/__init__.py +++ b/core/src/ten_runtime/binding/python/interface/ten/__init__.py @@ -6,6 +6,8 @@ # from .app import App from .extension import Extension +from .async_extension import AsyncExtension +from .async_ten_env import AsyncTenEnv from .addon import Addon from .decorator import ( register_addon_as_extension, @@ -28,7 +30,9 @@ "register_addon_as_extension_group", "App", "Extension", + "AsyncExtension", "TenEnv", + "AsyncTenEnv", "Cmd", "StatusCode", "VideoFrame", diff --git a/core/src/ten_runtime/binding/python/interface/ten/app.py b/core/src/ten_runtime/binding/python/interface/ten/app.py index 321a928652..7de98704b5 100644 --- a/core/src/ten_runtime/binding/python/interface/ten/app.py +++ b/core/src/ten_runtime/binding/python/interface/ten/app.py @@ -26,3 +26,6 @@ def on_configure(self, ten_env: TenEnv) -> None: def on_init(self, ten_env: TenEnv) -> None: ten_env.on_init_done() + + def on_deinit(self, ten_env: TenEnv) -> None: + ten_env.on_deinit_done() diff --git a/core/src/ten_runtime/binding/python/interface/ten/async_extension.py b/core/src/ten_runtime/binding/python/interface/ten/async_extension.py new file mode 100644 index 0000000000..6f9de65d22 --- /dev/null +++ b/core/src/ten_runtime/binding/python/interface/ten/async_extension.py @@ -0,0 +1,140 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import asyncio +import threading +from typing import final +from libten_runtime_python import _Extension +from .video_frame import VideoFrame +from .audio_frame import AudioFrame +from .ten_env import TenEnv +from .cmd import Cmd +from .data import Data +from .async_ten_env import AsyncTenEnv + + +class AsyncExtension(_Extension): + def __init__(self, name: str) -> None: + self._ten_stop_event = asyncio.Event() + + def __del__(self) -> None: + self._ten_stop_event.set() + if hasattr(self, "_ten_thread"): + self._ten_thread.join() + + async def _thread_routine(self, ten_env: TenEnv): + self._ten_loop = asyncio.get_running_loop() + self._async_ten_env = AsyncTenEnv( + ten_env, self._ten_loop, self._ten_thread + ) + + await self.on_configure(self._async_ten_env) + + # Suspend the thread until stopEvent is set. + await self._ten_stop_event.wait() + + await self.on_deinit(self._async_ten_env) + + async def _stop_thread(self): + self._ten_stop_event.set() + + @final + def _proxy_on_configure(self, ten_env: TenEnv) -> None: + # We pass the TenEnv object to another Python thread without worrying + # about the thread safety issue of the TenEnv API, because the actual + # execution logic of all TenEnv APIs occurs in the extension thread. + # We only need to ensure that the TenEnv object should remain valid + # while it is being used. The way to achieve this is to ensure that the + # Python thread remains alive until TenEnv.on_deinit_done is called. + self._ten_thread = threading.Thread( + target=asyncio.run, args=(self._thread_routine(ten_env),) + ) + self._ten_thread.start() + + @final + def _proxy_on_init(self, ten_env: TenEnv) -> None: + asyncio.run_coroutine_threadsafe( + self.on_init(self._async_ten_env), self._ten_loop + ) + + @final + def _proxy_on_start(self, ten_env: TenEnv) -> None: + asyncio.run_coroutine_threadsafe( + self.on_start(self._async_ten_env), self._ten_loop + ) + + @final + def _proxy_on_stop(self, ten_env: TenEnv) -> None: + asyncio.run_coroutine_threadsafe( + self.on_stop(self._async_ten_env), self._ten_loop + ) + + @final + def _proxy_on_deinit(self, ten_env: TenEnv) -> None: + asyncio.run_coroutine_threadsafe(self._stop_thread(), self._ten_loop) + + @final + def _proxy_on_cmd(self, ten_env: TenEnv, cmd: Cmd) -> None: + asyncio.run_coroutine_threadsafe( + self.on_cmd(self._async_ten_env, cmd), self._ten_loop + ) + + @final + def _proxy_on_data(self, ten_env: TenEnv, data: Data) -> None: + asyncio.run_coroutine_threadsafe( + self.on_data(self._async_ten_env, data), self._ten_loop + ) + + @final + def _proxy_on_video_frame( + self, ten_env: TenEnv, video_frame: VideoFrame + ) -> None: + asyncio.run_coroutine_threadsafe( + self.on_video_frame(self._async_ten_env, video_frame), + self._ten_loop, + ) + + @final + def _proxy_on_audio_frame( + self, ten_env: TenEnv, audio_frame: AudioFrame + ) -> None: + asyncio.run_coroutine_threadsafe( + self.on_audio_frame(self._async_ten_env, audio_frame), + self._ten_loop, + ) + + # Override these methods in your extension + + async def on_configure(self, async_ten_env: AsyncTenEnv) -> None: + async_ten_env.on_configure_done() + + async def on_init(self, async_ten_env: AsyncTenEnv) -> None: + async_ten_env.on_init_done() + + async def on_start(self, async_ten_env: AsyncTenEnv) -> None: + async_ten_env.on_start_done() + + async def on_stop(self, async_ten_env: AsyncTenEnv) -> None: + async_ten_env.on_stop_done() + + async def on_deinit(self, async_ten_env: AsyncTenEnv) -> None: + async_ten_env.on_deinit_done() + + async def on_cmd(self, async_ten_env: AsyncTenEnv, cmd: Cmd) -> None: + pass + + async def on_data(self, async_ten_env: AsyncTenEnv, data: Data) -> None: + pass + + async def on_video_frame( + self, async_ten_env: AsyncTenEnv, video_frame: VideoFrame + ) -> None: + pass + + async def on_audio_frame( + self, async_ten_env: AsyncTenEnv, audio_frame: AudioFrame + ) -> None: + pass diff --git a/core/src/ten_runtime/binding/python/interface/ten/async_ten_env.py b/core/src/ten_runtime/binding/python/interface/ten/async_ten_env.py new file mode 100644 index 0000000000..059a3d89dd --- /dev/null +++ b/core/src/ten_runtime/binding/python/interface/ten/async_ten_env.py @@ -0,0 +1,58 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +from asyncio import AbstractEventLoop +import asyncio +import threading +from .cmd import Cmd +from .cmd_result import CmdResult +from .ten_env import TenEnv + + +class AsyncTenEnv(TenEnv): + + def __init__( + self, ten_env: TenEnv, loop: AbstractEventLoop, thread: threading.Thread + ) -> None: + self._internal = ten_env._internal + self._ten_loop = loop + self._ten_thread = thread + ten_env._set_release_handler(lambda: self._on_release()) + + def __del__(self) -> None: + pass + + async def send_cmd(self, cmd: Cmd) -> CmdResult: + q = asyncio.Queue(1) + self._internal.send_cmd( + cmd, + lambda ten_env, result: asyncio.run_coroutine_threadsafe( + q.put(result), self._ten_loop + ), # type: ignore + ) + return await q.get() + + async def send_json(self, json_str: str) -> CmdResult: + q = asyncio.Queue(1) + self._internal.send_json( + json_str, + lambda ten_env, result: asyncio.run_coroutine_threadsafe( + q.put(result), self._ten_loop + ), # type: ignore + ) + return await q.get() + + def _deinit_routine(self) -> None: + self._ten_thread.join() + self._internal.on_deinit_done() + + def _on_release(self) -> None: + if hasattr(self, "_deinit_thread"): + self._deinit_thread.join() + + def on_deinit_done(self) -> None: + self._deinit_thread = threading.Thread(target=self._deinit_routine) + self._deinit_thread.start() diff --git a/core/src/ten_runtime/binding/python/interface/ten/extension.py b/core/src/ten_runtime/binding/python/interface/ten/extension.py index d4d0b22752..2ecb489604 100644 --- a/core/src/ten_runtime/binding/python/interface/ten/extension.py +++ b/core/src/ten_runtime/binding/python/interface/ten/extension.py @@ -33,26 +33,58 @@ def _proxy_on_configure(self, ten_env: TenEnv) -> None: def on_configure(self, ten_env: TenEnv) -> None: ten_env.on_configure_done() + @final + def _proxy_on_init(self, ten_env: TenEnv) -> None: + self.on_init(ten_env) + def on_init(self, ten_env: TenEnv) -> None: ten_env.on_init_done() + @final + def _proxy_on_start(self, ten_env: TenEnv) -> None: + self.on_start(ten_env) + def on_start(self, ten_env: TenEnv) -> None: ten_env.on_start_done() + @final + def _proxy_on_stop(self, ten_env: TenEnv) -> None: + self.on_stop(ten_env) + def on_stop(self, ten_env: TenEnv) -> None: ten_env.on_stop_done() + @final + def _proxy_on_deinit(self, ten_env: TenEnv) -> None: + self.on_deinit(ten_env) + def on_deinit(self, ten_env: TenEnv) -> None: ten_env.on_deinit_done() + @final + def _proxy_on_cmd(self, ten_env: TenEnv, cmd: Cmd) -> None: + self.on_cmd(ten_env, cmd) + def on_cmd(self, ten_env: TenEnv, cmd: Cmd) -> None: pass + @final + def _proxy_on_data(self, ten_env: TenEnv, data: Data) -> None: + self.on_data(ten_env, data) + def on_data(self, ten_env: TenEnv, data: Data) -> None: pass + @final + def _proxy_on_video_frame(self, ten_env: TenEnv, video_frame: VideoFrame) -> None: + self.on_video_frame(ten_env, video_frame) + def on_video_frame(self, ten_env: TenEnv, video_frame: VideoFrame) -> None: pass + @final + def _proxy_on_audio_frame(self, ten_env: TenEnv, audio_frame: AudioFrame) -> None: + self.on_audio_frame(ten_env, audio_frame) + def on_audio_frame(self, ten_env: TenEnv, audio_frame: AudioFrame) -> None: pass diff --git a/core/src/ten_runtime/binding/python/interface/ten/libten_runtime_python.pyi b/core/src/ten_runtime/binding/python/interface/ten/libten_runtime_python.pyi index 9300b0a165..38baeacef2 100644 --- a/core/src/ten_runtime/binding/python/interface/ten/libten_runtime_python.pyi +++ b/core/src/ten_runtime/binding/python/interface/ten/libten_runtime_python.pyi @@ -126,6 +126,10 @@ class _App: self, ten_env: _TenEnv, ) -> None: ... + def on_deinit( + self, + ten_env: _TenEnv, + ) -> None: ... class _Extension: def __init__(self, name: str): ... diff --git a/core/src/ten_runtime/binding/python/interface/ten/ten_env.py b/core/src/ten_runtime/binding/python/interface/ten/ten_env.py index bc15d7fc45..573e1dd10e 100644 --- a/core/src/ten_runtime/binding/python/interface/ten/ten_env.py +++ b/core/src/ten_runtime/binding/python/interface/ten/ten_env.py @@ -30,6 +30,13 @@ def __init__(self, internal_obj: _TenEnv) -> None: def __del__(self) -> None: pass + def _set_release_handler(self, handler: Callable[[], None]) -> None: + self._release_handler = handler + + def _on_release(self) -> None: + if hasattr(self, "_release_handler"): + self._release_handler() + def on_configure_done(self) -> None: from .addon import Addon diff --git a/core/src/ten_runtime/binding/python/native/app/app.c b/core/src/ten_runtime/binding/python/native/app/app.c index 17586366af..3d5d2ed113 100644 --- a/core/src/ten_runtime/binding/python/native/app/app.c +++ b/core/src/ten_runtime/binding/python/native/app/app.c @@ -74,7 +74,7 @@ static void proxy_on_configure(ten_app_t *app, ten_env_t *ten_env) { // achieving numerical consistency between PyGILState_Ensure and // PyGILState_Release, and only then will the Python thread state be // released. - ten_py_eval_save_thread(); + py_ten_env->py_thread_state = ten_py_eval_save_thread(); } else { // No need to release the GIL. } diff --git a/core/src/ten_runtime/binding/python/native/extension/extension.c b/core/src/ten_runtime/binding/python/native/extension/extension.c index ab40c99ec6..62f28e4a50 100644 --- a/core/src/ten_runtime/binding/python/native/extension/extension.c +++ b/core/src/ten_runtime/binding/python/native/extension/extension.c @@ -82,7 +82,7 @@ static void proxy_on_configure(ten_extension_t *extension, ten_env_t *ten_env) { // We should release the GIL but not destroy the PyThreadState. The // PyThreadState will not be released until the last extension calls // 'on_deinit_done' in the group. - ten_py_eval_save_thread(); + py_ten_env->py_thread_state = ten_py_eval_save_thread(); py_ten_env->need_to_release_gil_state = true; } @@ -112,7 +112,7 @@ static void proxy_on_init(ten_extension_t *extension, ten_env_t *ten_env) { TEN_ASSERT(py_ten_env, "Should not happen."); PyObject *py_res = - PyObject_CallMethod((PyObject *)py_extension, "on_init", "O", + PyObject_CallMethod((PyObject *)py_extension, "_proxy_on_init", "O", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env); Py_XDECREF(py_res); @@ -145,7 +145,7 @@ static void proxy_on_start(ten_extension_t *extension, ten_env_t *ten_env) { TEN_ASSERT(py_ten_env, "Should not happen."); PyObject *py_res = - PyObject_CallMethod((PyObject *)py_extension, "on_start", "O", + PyObject_CallMethod((PyObject *)py_extension, "_proxy_on_start", "O", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env); Py_XDECREF(py_res); @@ -178,7 +178,7 @@ static void proxy_on_stop(ten_extension_t *extension, ten_env_t *ten_env) { TEN_ASSERT(py_ten_env, "Should not happen."); PyObject *py_res = - PyObject_CallMethod((PyObject *)py_extension, "on_stop", "O", + PyObject_CallMethod((PyObject *)py_extension, "_proxy_on_stop", "O", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env); Py_XDECREF(py_res); @@ -211,7 +211,7 @@ static void proxy_on_deinit(ten_extension_t *extension, ten_env_t *ten_env) { TEN_ASSERT(py_ten_env, "Should not happen."); PyObject *py_res = - PyObject_CallMethod((PyObject *)py_extension, "on_deinit", "O", + PyObject_CallMethod((PyObject *)py_extension, "_proxy_on_deinit", "O", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env); Py_XDECREF(py_res); @@ -246,7 +246,7 @@ static void proxy_on_cmd(ten_extension_t *extension, ten_env_t *ten_env, ten_py_cmd_t *py_cmd = ten_py_cmd_wrap(cmd); PyObject *py_res = PyObject_CallMethod( - (PyObject *)py_extension, "on_cmd", "OO", + (PyObject *)py_extension, "_proxy_on_cmd", "OO", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env, py_cmd); Py_XDECREF(py_res); @@ -283,7 +283,7 @@ static void proxy_on_data(ten_extension_t *extension, ten_env_t *ten_env, ten_py_data_t *py_data = ten_py_data_wrap(data); PyObject *py_res = PyObject_CallMethod( - (PyObject *)py_extension, "on_data", "OO", + (PyObject *)py_extension, "_proxy_on_data", "OO", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env, py_data); Py_XDECREF(py_res); @@ -321,7 +321,7 @@ static void proxy_on_audio_frame(ten_extension_t *extension, ten_env_t *ten_env, ten_py_audio_frame_t *py_audio_frame = ten_py_audio_frame_wrap(audio_frame); PyObject *py_res = PyObject_CallMethod( - (PyObject *)py_extension, "on_audio_frame", "OO", + (PyObject *)py_extension, "_proxy_on_audio_frame", "OO", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env, py_audio_frame); Py_XDECREF(py_res); @@ -352,7 +352,7 @@ static void proxy_on_video_frame(ten_extension_t *extension, ten_env_t *ten_env, ten_py_video_frame_t *py_video_frame = ten_py_video_frame_wrap(video_frame); PyObject *py_res = PyObject_CallMethod( - py_extension, "on_video_frame", "OO", + py_extension, "_proxy_on_video_frame", "OO", ((ten_py_ten_env_t *)py_ten_env)->actual_py_ten_env, py_video_frame); Py_XDECREF(py_res); diff --git a/core/src/ten_runtime/binding/python/native/ten_env/ten_env.c b/core/src/ten_runtime/binding/python/native/ten_env/ten_env.c index 40b9f39484..ff8d1615bd 100644 --- a/core/src/ten_runtime/binding/python/native/ten_env/ten_env.c +++ b/core/src/ten_runtime/binding/python/native/ten_env/ten_env.c @@ -95,6 +95,7 @@ ten_py_ten_env_t *ten_py_ten_env_wrap(ten_env_t *ten_env) { py_ten_env->c_ten_env = ten_env; py_ten_env->c_ten_env_proxy = NULL; py_ten_env->need_to_release_gil_state = false; + py_ten_env->py_thread_state = NULL; py_ten_env->actual_py_ten_env = create_actual_py_ten_env_instance(py_ten_env); if (!py_ten_env->actual_py_ten_env) { diff --git a/core/src/ten_runtime/binding/python/native/ten_env/ten_env_on_deinit_done.c b/core/src/ten_runtime/binding/python/native/ten_env/ten_env_on_deinit_done.c index e079e38730..e876cb92c6 100644 --- a/core/src/ten_runtime/binding/python/native/ten_env/ten_env_on_deinit_done.c +++ b/core/src/ten_runtime/binding/python/native/ten_env/ten_env_on_deinit_done.c @@ -8,10 +8,13 @@ #include "include_internal/ten_runtime/binding/python/common.h" #include "include_internal/ten_runtime/binding/python/common/common.h" +#include "include_internal/ten_runtime/binding/python/common/error.h" #include "include_internal/ten_runtime/binding/python/ten_env/ten_env.h" #include "include_internal/ten_runtime/ten_env/ten_env.h" +#include "pystate.h" #include "ten_runtime/ten_env/internal/on_xxx_done.h" #include "ten_runtime/ten_env_proxy/ten_env_proxy.h" +#include "ten_utils/macro/check.h" #include "ten_utils/macro/mark.h" static void ten_env_proxy_notify_on_deinit_done(ten_env_t *ten_env, @@ -29,6 +32,20 @@ static void ten_env_proxy_notify_on_deinit_done(ten_env_t *ten_env, ten_py_ten_env_t *py_ten_env = user_data; TEN_ASSERT(py_ten_env, "Should not happen."); + // Notify the Python side to do the cleanup. + // About to call the Python function, so it's necessary to ensure that the + // GIL has been acquired. + PyGILState_STATE prev_state = ten_py_gil_state_ensure(); + + PyObject *py_res = + PyObject_CallMethod(py_ten_env->actual_py_ten_env, "_on_release", NULL); + Py_XDECREF(py_res); + + bool err_occurred = ten_py_check_and_clear_py_error(); + TEN_ASSERT(!err_occurred, "Should not happen."); + + ten_py_gil_state_release(prev_state); + if (py_ten_env->c_ten_env_proxy) { TEN_ASSERT( ten_env_proxy_get_thread_cnt(py_ten_env->c_ten_env_proxy, NULL) == 1, @@ -49,9 +66,11 @@ static void ten_env_proxy_notify_on_deinit_done(ten_env_t *ten_env, if (py_ten_env->need_to_release_gil_state) { if (!ten_py_is_holding_gil()) { + TEN_ASSERT(py_ten_env->py_thread_state != NULL, "Should not happen."); + // The gil is not held by the current thread, so we have to acquire the // gil before we can release the gil state. - ten_py_eval_restore_thread(NULL); + ten_py_eval_restore_thread(py_ten_env->py_thread_state); // Release the gil state and the gil. ten_py_gil_state_release(PyGILState_UNLOCKED); diff --git a/packages/core_extensions/BUILD.gn b/packages/core_extensions/BUILD.gn index 7b14c973d0..66e139aea5 100644 --- a/packages/core_extensions/BUILD.gn +++ b/packages/core_extensions/BUILD.gn @@ -14,6 +14,7 @@ group("core_extensions") { if (ten_enable_python_binding) { deps += [ + "default_async_extension_python", "default_extension_python", "py_init_extension_cpp", ] diff --git a/packages/core_extensions/default_async_extension_python/BUILD.gn b/packages/core_extensions/default_async_extension_python/BUILD.gn new file mode 100644 index 0000000000..a762096779 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/BUILD.gn @@ -0,0 +1,42 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +import("//build/feature/ten_package.gni") +import("//build/ten_runtime/feature/publish.gni") +import("//build/ten_runtime/glob.gni") +import("//build/ten_runtime/options.gni") + +ten_package("default_async_extension_python") { + package_kind = "extension" + + resources = [ + "BUILD_release.gn.tent=>BUILD.gn.tent", + "BUILD_release.gn=>BUILD.gn", + "README.md", + "README.md.tent", + "__init__.py", + "__init__.py.tent", + "addon.py", + "addon.py.tent", + "extension.py", + "extension.py.tent", + "log.py", + "log.py.tent", + "manifest.json", + "manifest.json.tent", + "property.json", + "tests", + ] + + deps = [ "//core/src/ten_runtime" ] +} + +if (ten_enable_package_manager) { + ten_package_publish("upload_default_async_extension_python_to_server") { + base_dir = rebase_path( + "${root_out_dir}/ten_packages/extension/default_async_extension_python") + deps = [ ":default_async_extension_python" ] + } +} diff --git a/packages/core_extensions/default_async_extension_python/BUILD_release.gn b/packages/core_extensions/default_async_extension_python/BUILD_release.gn new file mode 100644 index 0000000000..5053ef7095 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/BUILD_release.gn @@ -0,0 +1,20 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +import("//build/feature/ten_package.gni") + +ten_package("default_async_extension_python") { + package_kind = "extension" + + resources = [ + "__init__.py", + "addon.py", + "extension.py", + "log.py", + "manifest.json", + "property.json", + "tests", + ] +} diff --git a/packages/core_extensions/default_async_extension_python/BUILD_release.gn.tent b/packages/core_extensions/default_async_extension_python/BUILD_release.gn.tent new file mode 100644 index 0000000000..186badc385 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/BUILD_release.gn.tent @@ -0,0 +1,20 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +import("//build/feature/ten_package.gni") + +ten_package("{{package_name}}") { + package_kind = "extension" + + resources = [ + "__init__.py", + "addon.py", + "extension.py", + "log.py", + "manifest.json", + "property.json", + "tests", + ] +} diff --git a/packages/core_extensions/default_async_extension_python/LICENSE b/packages/core_extensions/default_async_extension_python/LICENSE new file mode 100644 index 0000000000..e37872ecc6 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/LICENSE @@ -0,0 +1,13 @@ +Copyright © 2024 Agora + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/core_extensions/default_async_extension_python/README.md b/packages/core_extensions/default_async_extension_python/README.md new file mode 100644 index 0000000000..2f995d526a --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/README.md @@ -0,0 +1,29 @@ +# default_extension_python + + + +## Features + + + +- xxx feature + +## API + +Refer to `api` definition in [manifest.json] and default values in [property.json](property.json). + + + +## Development + +### Build + + + +### Unit test + + + +## Misc + + diff --git a/packages/core_extensions/default_async_extension_python/README.md.tent b/packages/core_extensions/default_async_extension_python/README.md.tent new file mode 100644 index 0000000000..a327c5694c --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/README.md.tent @@ -0,0 +1,29 @@ +# {{package_name}} + + + +## Features + + + +- xxx feature + +## API + +Refer to `api` definition in [manifest.json] and default values in [property.json](property.json). + + + +## Development + +### Build + + + +### Unit test + + + +## Misc + + diff --git a/packages/core_extensions/default_async_extension_python/__init__.py b/packages/core_extensions/default_async_extension_python/__init__.py new file mode 100644 index 0000000000..2cc80a0ced --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/__init__.py @@ -0,0 +1,9 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +from . import addon +from .log import logger + +logger.info("default_async_extension_python extension loaded") diff --git a/packages/core_extensions/default_async_extension_python/__init__.py.tent b/packages/core_extensions/default_async_extension_python/__init__.py.tent new file mode 100644 index 0000000000..9c752bfc8d --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/__init__.py.tent @@ -0,0 +1,9 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +from . import addon +from .log import logger + +logger.info("{{package_name}} extension loaded") diff --git a/packages/core_extensions/default_async_extension_python/addon.py b/packages/core_extensions/default_async_extension_python/addon.py new file mode 100644 index 0000000000..d2e813b06b --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/addon.py @@ -0,0 +1,20 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +from ten import ( + Addon, + register_addon_as_extension, + TenEnv, +) +from .extension import DefaultAsyncExtension +from .log import logger + + +@register_addon_as_extension("default_async_extension_python") +class DefaultAsyncExtensionAddon(Addon): + + def on_create_instance(self, ten_env: TenEnv, name: str, context) -> None: + logger.info("DefaultAsyncExtensionAddon on_create_instance") + ten_env.on_create_instance_done(DefaultAsyncExtension(name), context) diff --git a/packages/core_extensions/default_async_extension_python/addon.py.tent b/packages/core_extensions/default_async_extension_python/addon.py.tent new file mode 100644 index 0000000000..9c1d3fe19a --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/addon.py.tent @@ -0,0 +1,20 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +from ten import ( + Addon, + register_addon_as_extension, + TenEnv, +) +from .extension import {{class_name_prefix}}Extension +from .log import logger + + +@register_addon_as_extension("{{package_name}}") +class {{class_name_prefix}}ExtensionAddon(Addon): + + def on_create_instance(self, ten_env: TenEnv, name: str, context) -> None: + logger.info("{{class_name_prefix}}ExtensionAddon on_create_instance") + ten_env.on_create_instance_done({{class_name_prefix}}Extension(name), context) diff --git a/packages/core_extensions/default_async_extension_python/extension.py b/packages/core_extensions/default_async_extension_python/extension.py new file mode 100644 index 0000000000..64875869d3 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/extension.py @@ -0,0 +1,73 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +from ten import ( + AudioFrame, + VideoFrame, + AsyncExtension, + AsyncTenEnv, + Cmd, + StatusCode, + CmdResult, + Data, +) + + +class DefaultAsyncExtension(AsyncExtension): + async def on_init(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_init") + ten_env.on_init_done() + + async def on_start(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_start") + + # TODO: read properties, initialize resources + + ten_env.on_start_done() + + async def on_stop(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_stop") + + # TODO: clean up resources + + ten_env.on_stop_done() + + async def on_deinit(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_deinit") + ten_env.on_deinit_done() + + async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None: + cmd_name = cmd.get_name() + ten_env.log_debug("on_cmd name {}".format(cmd_name)) + + # TODO: process cmd + + cmd_result = CmdResult.create(StatusCode.OK) + ten_env.return_result(cmd_result, cmd) + + async def on_data(self, ten_env: AsyncTenEnv, data: Data) -> None: + data_name = data.get_name() + ten_env.log_debug("on_data name {}".format(data_name)) + + # TODO: process data + pass + + async def on_audio_frame( + self, ten_env: AsyncTenEnv, audio_frame: AudioFrame + ) -> None: + audio_frame_name = audio_frame.get_name() + ten_env.log_debug("on_audio_frame name {}".format(audio_frame_name)) + + # TODO: process audio frame + pass + + async def on_video_frame( + self, ten_env: AsyncTenEnv, video_frame: VideoFrame + ) -> None: + video_frame_name = video_frame.get_name() + ten_env.log_debug("on_video_frame name {}".format(video_frame_name)) + + # TODO: process video frame + pass diff --git a/packages/core_extensions/default_async_extension_python/extension.py.tent b/packages/core_extensions/default_async_extension_python/extension.py.tent new file mode 100644 index 0000000000..49eb8aebd0 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/extension.py.tent @@ -0,0 +1,73 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +from ten import ( + AudioFrame, + VideoFrame, + AsyncExtension, + AsyncTenEnv, + Cmd, + StatusCode, + CmdResult, + Data, +) + + +class {{class_name_prefix}}Extension(AsyncExtension): + async def on_init(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_init") + ten_env.on_init_done() + + async def on_start(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_start") + + # TODO: read properties, initialize resources + + ten_env.on_start_done() + + async def on_stop(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_stop") + + # TODO: clean up resources + + ten_env.on_stop_done() + + async def on_deinit(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_deinit") + ten_env.on_deinit_done() + + async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None: + cmd_name = cmd.get_name() + ten_env.log_debug("on_cmd name {}".format(cmd_name)) + + # TODO: process cmd + + cmd_result = CmdResult.create(StatusCode.OK) + ten_env.return_result(cmd_result, cmd) + + async def on_data(self, ten_env: AsyncTenEnv, data: Data) -> None: + data_name = data.get_name() + ten_env.log_debug("on_data name {}".format(data_name)) + + # TODO: process data + pass + + async def on_audio_frame( + self, ten_env: AsyncTenEnv, audio_frame: AudioFrame + ) -> None: + audio_frame_name = audio_frame.get_name() + ten_env.log_debug("on_audio_frame name {}".format(audio_frame_name)) + + # TODO: process audio frame + pass + + async def on_video_frame( + self, ten_env: AsyncTenEnv, video_frame: VideoFrame + ) -> None: + video_frame_name = video_frame.get_name() + ten_env.log_debug("on_video_frame name {}".format(video_frame_name)) + + # TODO: process video frame + pass diff --git a/packages/core_extensions/default_async_extension_python/log.py b/packages/core_extensions/default_async_extension_python/log.py new file mode 100644 index 0000000000..8980ea64df --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/log.py @@ -0,0 +1,20 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +import logging + +logger = logging.getLogger("default_async_extension_python") +logger.setLevel(logging.INFO) + +formatter_str = ( + "%(asctime)s - %(name)s - %(levelname)s - %(process)d - " + "[%(filename)s:%(lineno)d] - %(message)s" +) +formatter = logging.Formatter(formatter_str) + +console_handler = logging.StreamHandler() +console_handler.setFormatter(formatter) + +logger.addHandler(console_handler) diff --git a/packages/core_extensions/default_async_extension_python/log.py.tent b/packages/core_extensions/default_async_extension_python/log.py.tent new file mode 100644 index 0000000000..5f9bd6b29f --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/log.py.tent @@ -0,0 +1,20 @@ +# +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0. +# See the LICENSE file for more information. +# +import logging + +logger = logging.getLogger("{{package_name}}") +logger.setLevel(logging.INFO) + +formatter_str = ( + "%(asctime)s - %(name)s - %(levelname)s - %(process)d - " + "[%(filename)s:%(lineno)d] - %(message)s" +) +formatter = logging.Formatter(formatter_str) + +console_handler = logging.StreamHandler() +console_handler.setFormatter(formatter) + +logger.addHandler(console_handler) diff --git a/packages/core_extensions/default_async_extension_python/manifest.json b/packages/core_extensions/default_async_extension_python/manifest.json new file mode 100644 index 0000000000..0e020f0c5d --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/manifest.json @@ -0,0 +1,24 @@ +{ + "type": "extension", + "name": "default_async_extension_python", + "version": "0.3.0-alpha", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_python", + "version": "0.3.0-alpha" + } + ], + "package": { + "include": [ + "manifest.json", + "property.json", + "BUILD.gn", + "**.tent", + "**.py", + "README.md", + "tests/**" + ] + }, + "api": {} +} \ No newline at end of file diff --git a/packages/core_extensions/default_async_extension_python/manifest.json.tent b/packages/core_extensions/default_async_extension_python/manifest.json.tent new file mode 100644 index 0000000000..8d62e55140 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/manifest.json.tent @@ -0,0 +1,24 @@ +{ + "type": "extension", + "name": "{{package_name}}", + "version": "0.3.0-alpha", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_python", + "version": "0.3.0-alpha" + } + ], + "package": { + "include": [ + "manifest.json", + "property.json", + "BUILD.gn", + "**.tent", + "**.py", + "README.md", + "tests/**" + ] + }, + "api": {} +} \ No newline at end of file diff --git a/packages/core_extensions/default_async_extension_python/property.json b/packages/core_extensions/default_async_extension_python/property.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/property.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/core_extensions/default_async_extension_python/tests/bin/start b/packages/core_extensions/default_async_extension_python/tests/bin/start new file mode 100755 index 0000000000..4723f241bd --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/tests/bin/start @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +cd "$(dirname "${BASH_SOURCE[0]}")/../.." + +export PYTHONPATH=.ten/app/ten_packages/system/ten_runtime_python/lib:.ten/app/ten_packages/system/ten_runtime_python/interface + +# If the Python app imports some modules that are compiled with a different +# version of libstdc++ (ex: PyTorch), the Python app may encounter confusing +# errors. To solve this problem, we can preload the correct version of +# libstdc++. +# +# export LD_PRELOAD=/lib/x86_64-linux-gnu/libstdc++.so.6 +# +# Another solution is to make sure the module 'ten_runtime_python' is imported +# _after_ the module that requires another version of libstdc++ is imported. +# +# Refer to https://github.com/pytorch/pytorch/issues/102360?from_wecom=1#issuecomment-1708989096 + +pytest -s tests/ diff --git a/packages/core_extensions/default_async_extension_python/tests/test_basic.py b/packages/core_extensions/default_async_extension_python/tests/test_basic.py new file mode 100644 index 0000000000..c3755f449a --- /dev/null +++ b/packages/core_extensions/default_async_extension_python/tests/test_basic.py @@ -0,0 +1,36 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +from pathlib import Path +from ten import ExtensionTester, TenEnvTester, Cmd, CmdResult, StatusCode + + +class ExtensionTesterBasic(ExtensionTester): + def check_hello(self, ten_env: TenEnvTester, result: CmdResult): + statusCode = result.get_status_code() + print("receive hello_world, status:" + str(statusCode)) + + if statusCode == StatusCode.OK: + ten_env.stop_test() + + def on_start(self, ten_env: TenEnvTester) -> None: + new_cmd = Cmd.create("hello_world") + + print("send hello_world") + ten_env.send_cmd( + new_cmd, + lambda ten_env, result: self.check_hello(ten_env, result), + ) + + print("tester on_start_done") + ten_env.on_start_done() + + +def test_basic(): + tester = ExtensionTesterBasic() + tester.add_addon_base_dir(str(Path(__file__).resolve().parent.parent)) + tester.set_test_mode_single("default_async_extension_python") + tester.run() diff --git a/packages/example_extensions/aio_http_server_python/main.py b/packages/example_extensions/aio_http_server_python/main.py index eade33052d..5f630c6950 100644 --- a/packages/example_extensions/aio_http_server_python/main.py +++ b/packages/example_extensions/aio_http_server_python/main.py @@ -3,28 +3,28 @@ # Licensed under the Apache License, Version 2.0. # See the LICENSE file for more information. # +import asyncio import json -import threading from aiohttp import web, web_request, WSMsgType -import asyncio from ten import ( Addon, - Extension, + AsyncExtension, register_addon_as_extension, TenEnv, Cmd, CmdResult, StatusCode, + AsyncTenEnv, ) -class HttpServerExtension(Extension): +class HttpServerExtension(AsyncExtension): async def default_handler(self, request: web_request.Request): - # parse the json body + # Parse the json body. try: data = await request.json() except Exception as e: - print("Error: " + str(e)) + self.ten_env.log_error("Error: " + str(e)) return web.Response(status=400, text="Bad request") cmd_result = None @@ -37,16 +37,23 @@ async def default_handler(self, request: web_request.Request): else: # If the command is a 'close_app' command, send it to the app. if "type" in data["_ten"] and data["_ten"]["type"] == "close_app": - close_app_cmd_json = """{"_ten":{"type":"close_app","dest":[{"app":"localhost"}]}}""" - self.tenEnv.send_json(close_app_cmd_json, None) + close_app_cmd_json = ( + '{"_ten":{"type":"close_app",' + '"dest":[{"app":"localhost"}]}}' + ) + asyncio.create_task(self.ten_env.send_json(close_app_cmd_json)) return web.Response(status=200, text="OK") elif "name" in data["_ten"]: - # send the command to the TEN runtime + # Send the command to the TEN runtime. data["method"] = method data["url"] = url cmd = Cmd.create_from_json(json.dumps(data)) - # send 'test' command to the TEN runtime and wait for the result - cmd_result = await self.send_cmd_async(self.tenEnv, cmd) + + # Send the command to the TEN runtime and wait for the result. + if cmd is None: + return web.Response(status=400, text="Bad request") + + cmd_result = await self.ten_env.send_cmd(cmd) else: return web.Response(status=404, text="Not found") @@ -55,7 +62,7 @@ async def default_handler(self, request: web_request.Request): detail = cmd_result.get_property_string("detail") return web.Response(text=detail) except Exception as e: - print("Error: " + str(e)) + self.ten_env.log_error("Error: " + str(e)) return web.Response(status=500, text="Internal server error") else: return web.Response(status=500, text="Internal server error") @@ -71,7 +78,9 @@ async def default_ws_handler(self, request: web_request.Request): else: await ws.send_str("some websocket message payload") elif msg.type == WSMsgType.ERROR: - print("ws connection closed with exception %s" % ws.exception()) + self.ten_env.log_error( + "ws connection closed with exception %s" % ws.exception() + ) return ws @@ -91,78 +100,41 @@ async def start_server(self, host, port): self.tcpSite = web.TCPSite(runner, host, port) await self.tcpSite.start() - async def __thread_routine(self, ten_env: TenEnv): - print("HttpServerExtension __thread_routine start") - - self.loop = asyncio.get_running_loop() - - await self.start_server("0.0.0.0", self.server_port) - - ten_env.on_start_done() - - # Suspend the thread until stopEvent is set - await self.stopEvent.wait() - - await self.webApp.shutdown() - await self.webApp.cleanup() - - async def stop_thread(self): - self.stopEvent.set() - - async def send_cmd_async(self, ten_env: TenEnv, cmd: Cmd) -> CmdResult: - print("HttpServerExtension send_cmd_async") - q = asyncio.Queue(1) - ten_env.send_cmd( - cmd, - lambda ten_env, result: asyncio.run_coroutine_threadsafe( - q.put(result), self.loop - ), # type: ignore - ) - return await q.get() - def __init__(self, name: str) -> None: super().__init__(name) self.name = name - self.stopEvent = asyncio.Event() - def on_init(self, ten_env: TenEnv) -> None: + async def on_init(self, ten_env: AsyncTenEnv) -> None: + self.ten_env = ten_env ten_env.on_init_done() - def on_start(self, ten_env: TenEnv) -> None: - print("HttpServerExtension on_start") + async def on_start(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("HttpServerExtension on_start") try: self.server_port = ten_env.get_property_int("server_port") except Exception as e: - print("Could not read 'server_port' from properties." + str(e)) + ten_env.log_error( + "Could not read 'server_port' from properties." + str(e) + ) self.server_port = 8002 - self.tenEnv = ten_env - - self.thread = threading.Thread( - target=asyncio.run, args=(self.__thread_routine(ten_env),) - ) + await self.start_server("0.0.0.0", self.server_port) - # Then 'on_start_done' will be called in the thread - self.thread.start() + ten_env.on_start_done() - def on_deinit(self, ten_env: TenEnv) -> None: - print("HttpServerExtension on_deinit") + async def on_deinit(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("HttpServerExtension on_deinit") ten_env.on_deinit_done() - def on_cmd(self, ten_env: TenEnv, cmd: Cmd) -> None: - print("HttpServerExtension on_cmd") + async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None: + ten_env.log_debug("HttpServerExtension on_cmd") - # Not supported command + # Not supported command. ten_env.return_result(CmdResult.create(StatusCode.ERROR), cmd) - def on_stop(self, ten_env: TenEnv) -> None: - print("HttpServerExtension on_stop") - - if self.thread.is_alive(): - asyncio.run_coroutine_threadsafe(self.stop_thread(), self.loop) - self.thread.join() - + async def on_stop(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("HttpServerExtension on_stop") ten_env.on_stop_done() diff --git a/tests/ten_runtime/integration/go/start_app_sync_go/start_app_sync_go_app/main.go b/tests/ten_runtime/integration/go/start_app_sync_go/start_app_sync_go_app/main.go index 757e83cc83..397220a640 100644 --- a/tests/ten_runtime/integration/go/start_app_sync_go/start_app_sync_go_app/main.go +++ b/tests/ten_runtime/integration/go/start_app_sync_go/start_app_sync_go_app/main.go @@ -18,9 +18,19 @@ type defaultApp struct { func (p *defaultApp) OnDeinit(tenEnv ten.TenEnv) { fmt.Println("DefaultApp onDeinit") + value, _ := tenEnv.GetPropertyString("key") + if value != "value" { + panic("failed to get property.") + } + tenEnv.OnDeinitDone() } +func (p *defaultApp) OnInit(tenEnv ten.TenEnv) { + tenEnv.SetPropertyString("key", "value") + tenEnv.OnInitDone() +} + func appRoutine(app ten.App, stopped chan<- struct{}) { app.Run(false) stopped <- struct{}{} diff --git a/tests/ten_runtime/integration/pytest.ini b/tests/ten_runtime/integration/pytest.ini index 856f195dad..9135415f10 100644 --- a/tests/ten_runtime/integration/pytest.ini +++ b/tests/ten_runtime/integration/pytest.ini @@ -1,6 +1,7 @@ [pytest] addopts = --ignore=tests/ten_runtime/integration/python/async_io_basic_python/async_io_basic_python_app/ten_packages/extension/default_extension_python/tests/ + --ignore=tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/ten_packages/extension/default_async_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/cpp_app_multi_process_python/cpp_app_multi_process_python_app_source/out/linux/x64/app/cpp_app_multi_process_python_app_source/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/cpp_app_multi_process_python/cpp_app_multi_process_python_app_source/out/linux/x64/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/cpp_app_multi_process_python/cpp_app_multi_process_python_app_source/ten_packages/extension/default_extension_python/tests/ @@ -13,6 +14,7 @@ addopts = --ignore=tests/ten_runtime/integration/python/go_app_python/go_app_python_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/go_app_cythonize/go_app_cythonize_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/go_app_partially_cythonize/go_app_partially_cythonize_app/ten_packages/extension/default_extension_python/tests/ + --ignore=tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/large_json_python/large_json_python_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/multi_process_python/multi_process_python_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/multiple_results_python/multiple_results_python_app/ten_packages/extension/default_extension_python/tests/ @@ -24,4 +26,6 @@ addopts = --ignore=tests/ten_runtime/integration/python/send_recv_image_python/send_recv_image_python_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/send_recv_pcm_python/send_recv_pcm_python_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/standalone_test_python/default_extension_python/tests/ + --ignore=tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/ten_packages/extension/default_extension_python/tests/ + --ignore=tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/ten_packages/extension/default_extension_python/tests/ --ignore=tests/ten_runtime/integration/python/unused_addon_python/unused_addon_python_app/ten_packages/extension/default_extension_python/tests/ diff --git a/tests/ten_runtime/integration/python/BUILD.gn b/tests/ten_runtime/integration/python/BUILD.gn index dbbdf82922..5ec10dd537 100644 --- a/tests/ten_runtime/integration/python/BUILD.gn +++ b/tests/ten_runtime/integration/python/BUILD.gn @@ -18,10 +18,12 @@ group("python") { if (ten_enable_python_binding) { deps += [ "aio_http_server_python", + "async_extension_basic_python", "async_io_basic_python", "cpp_app_multi_process_python", "cpp_app_python", "get_set_prop_python", + "go_app_async_extension_python", "large_json_python", "multi_process_python", "multiple_results_python", @@ -33,6 +35,8 @@ group("python") { "send_recv_image_python", "send_recv_pcm_python", "standalone_test_python", + "two_async_exts_one_group_python", + "two_async_exts_python", ] if (ten_enable_python_binding && ten_enable_go_binding) { diff --git a/tests/ten_runtime/integration/python/async_extension_basic_python/BUILD.gn b/tests/ten_runtime/integration/python/async_extension_basic_python/BUILD.gn new file mode 100644 index 0000000000..4828c81873 --- /dev/null +++ b/tests/ten_runtime/integration/python/async_extension_basic_python/BUILD.gn @@ -0,0 +1,50 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import("//build/ten_runtime/feature/test.gni") +import("//build/ten_runtime/ten.gni") + +ten_package_test_prepare_app("async_extension_basic_python_app") { + src_app = "default_app_python" + src_app_language = "python" + generated_app_src_root_dir_name = "async_extension_basic_python_app" + + replace_files_after_install_app = [ + "async_extension_basic_python_app/manifest.json", + "async_extension_basic_python_app/property.json", + ] + + replace_files_after_install_all = [ "async_extension_basic_python_app/ten_packages/extension/default_async_extension_python/extension.py" ] + + if (ten_enable_package_manager) { + deps = [ + "//core/src/ten_manager", + "//packages/core_apps/default_app_python:upload_default_app_python_to_server", + "//packages/core_extensions/default_async_extension_python:upload_default_async_extension_python_to_server", + "//packages/example_extensions/simple_echo_cpp:upload_simple_echo_cpp_to_server", + "//packages/example_extensions/simple_http_server_cpp:upload_simple_http_server_cpp_to_server", + ] + } +} + +ten_package_test_prepare_auxiliary_resources( + "async_extension_basic_python_test_files") { + resources = [ + "//tests/ten_runtime/integration/common=>common", + "__init__.py", + "test_case.py", + ] + if (enable_sanitizer) { + resources += [ "//tests/ten_runtime/integration/tools/use_asan_lib_marker=>use_asan_lib_marker" ] + } +} + +group("async_extension_basic_python") { + deps = [ + ":async_extension_basic_python_app", + ":async_extension_basic_python_test_files", + ] +} diff --git a/tests/ten_runtime/integration/python/async_extension_basic_python/__init__.py b/tests/ten_runtime/integration/python/async_extension_basic_python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/manifest.json b/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/manifest.json new file mode 100644 index 0000000000..bbb121767b --- /dev/null +++ b/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/manifest.json @@ -0,0 +1,24 @@ +{ + "dependencies": [ + { + "type": "system", + "name": "ten_runtime", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_http_server_cpp", + "version": "0.1.0" + }, + { + "type": "extension", + "name": "default_async_extension_python", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "version": "0.1.0" + } + ] +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/property.json b/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/property.json new file mode 100644 index 0000000000..97b14a4664 --- /dev/null +++ b/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/property.json @@ -0,0 +1,76 @@ +{ + "_ten": { + "log_level": 2, + "predefined_graphs": [ + { + "name": "default", + "auto_start": true, + "nodes": [ + { + "type": "extension_group", + "name": "default_extension_group", + "addon": "default_extension_group" + }, + { + "type": "extension_group", + "name": "test", + "addon": "default_extension_group" + }, + { + "type": "extension", + "name": "simple_http_server_cpp", + "addon": "simple_http_server_cpp", + "extension_group": "default_extension_group", + "property": { + "server_port": 8002 + } + }, + { + "type": "extension", + "name": "default_async_extension_python", + "addon": "default_async_extension_python", + "extension_group": "test" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "addon": "simple_echo_cpp", + "extension_group": "default_extension_group" + } + ], + "connections": [ + { + "extension_group": "default_extension_group", + "extension": "simple_http_server_cpp", + "cmd": [ + { + "name": "test", + "dest": [ + { + "extension_group": "test", + "extension": "default_async_extension_python" + } + ] + } + ] + }, + { + "extension_group": "test", + "extension": "default_async_extension_python", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "default_extension_group", + "extension": "simple_echo_cpp" + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/ten_packages/extension/default_async_extension_python/extension.py b/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/ten_packages/extension/default_async_extension_python/extension.py new file mode 100644 index 0000000000..5556ce3f8f --- /dev/null +++ b/tests/ten_runtime/integration/python/async_extension_basic_python/async_extension_basic_python_app/ten_packages/extension/default_async_extension_python/extension.py @@ -0,0 +1,54 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import asyncio +from ten import ( + AsyncExtension, + AsyncTenEnv, + Cmd, +) + + +class DefaultAsyncExtension(AsyncExtension): + def __init__(self, name: str) -> None: + super().__init__(name) + self.name = name + + async def on_configure(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_configure_done() + + async def on_init(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_init_done() + + async def on_start(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.log_debug("on_start") + ten_env.on_start_done() + + async def on_deinit(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_deinit_done() + + async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None: + cmd_json = cmd.to_json() + ten_env.log_debug(f"DefaultExtension on_cmd: {cmd_json}") + + # Mock async operation, e.g. network, file I/O. + await asyncio.sleep(0.5) + + # Send a new command to other extensions and wait for the result. The + # result will be returned to the original sender. + new_cmd = Cmd.create("hello") + cmd_result = await ten_env.send_cmd(new_cmd) + ten_env.return_result(cmd_result, cmd) + + async def on_stop(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("DefaultExtension on_stop") + + await asyncio.sleep(0.5) + ten_env.on_stop_done() diff --git a/tests/ten_runtime/integration/python/async_extension_basic_python/test_case.py b/tests/ten_runtime/integration/python/async_extension_basic_python/test_case.py new file mode 100644 index 0000000000..ea089778ab --- /dev/null +++ b/tests/ten_runtime/integration/python/async_extension_basic_python/test_case.py @@ -0,0 +1,136 @@ +""" +Test async_extension_basic_python. +""" + +import subprocess +import os +import sys +from sys import stdout +from .common import http + + +def http_request(): + return http.post( + "http://127.0.0.1:8002/", + { + "_ten": { + "name": "test", + }, + }, + ) + + +def test_async_extension_basic_python(): + """Test client and app server.""" + base_path = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.join(base_path, "../../../../../") + + # Create virtual environment. + venv_dir = os.path.join(base_path, "venv") + subprocess.run([sys.executable, "-m", "venv", venv_dir]) + + my_env = os.environ.copy() + + # Set the required environment variables for the test. + my_env["PYTHONMALLOC"] = "malloc" + my_env["PYTHONDEVMODE"] = "1" + + # Launch virtual environment. + my_env["VIRTUAL_ENV"] = venv_dir + my_env["PATH"] = os.path.join(venv_dir, "bin") + os.pathsep + my_env["PATH"] + + if sys.platform == "win32": + print("test_async_extension_basic_python doesn't support win32") + assert False + elif sys.platform == "darwin": + # client depends on some libraries in the TEN app. + my_env["DYLD_LIBRARY_PATH"] = os.path.join( + base_path, "async_extension_basic_python_app/lib" + ) + else: + # client depends on some libraries in the TEN app. + my_env["LD_LIBRARY_PATH"] = os.path.join( + base_path, "async_extension_basic_python_app/lib" + ) + + app_root_path = os.path.join(base_path, "async_extension_basic_python_app") + + tman_install_cmd = [ + os.path.join(root_dir, "ten_manager/bin/tman"), + "--config-file", + os.path.join(root_dir, "tests/local_registry/config.json"), + "install", + ] + + tman_install_process = subprocess.Popen( + tman_install_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + tman_install_process.wait() + + bootstrap_cmd = os.path.join( + base_path, "async_extension_basic_python_app/bin/bootstrap" + ) + + bootstrap_process = subprocess.Popen( + bootstrap_cmd, stdout=stdout, stderr=subprocess.STDOUT, env=my_env + ) + bootstrap_process.wait() + + if sys.platform == "linux": + if os.path.exists(os.path.join(base_path, "use_asan_lib_marker")): + libasan_path = os.path.join( + base_path, + "async_extension_basic_python_app/ten_packages/system/ten_runtime/lib/libasan.so", + ) + + if os.path.exists(libasan_path): + my_env["LD_PRELOAD"] = libasan_path + + server_cmd = os.path.join( + base_path, "async_extension_basic_python_app/bin/start" + ) + + server = subprocess.Popen( + server_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + + is_started = http.is_app_started("127.0.0.1", 8002, 30) + if not is_started: + print( + "The async_extension_basic_python is not started after 30 seconds." + ) + + server.kill() + exit_code = server.wait() + print("The exit code of async_extension_basic_python: ", exit_code) + + assert exit_code == 0 + assert 0 + + return + + try: + resp = http_request() + assert resp != 500 + print(resp) + + finally: + is_stopped = http.stop_app("127.0.0.1", 8002, 30) + if not is_stopped: + print( + "The async_extension_basic_python can not stop after 30 seconds." + ) + server.kill() + + exit_code = server.wait() + print("The exit code of async_extension_basic_python: ", exit_code) + + assert exit_code == 0 diff --git a/tests/ten_runtime/integration/python/go_app_async_extension_python/BUILD.gn b/tests/ten_runtime/integration/python/go_app_async_extension_python/BUILD.gn new file mode 100644 index 0000000000..462fc2cb4a --- /dev/null +++ b/tests/ten_runtime/integration/python/go_app_async_extension_python/BUILD.gn @@ -0,0 +1,55 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import("//build/ten_runtime/feature/test.gni") +import("//build/ten_runtime/ten.gni") + +ten_package_test_prepare_app("go_app_async_extension_python_app") { + src_app = "default_app_go" + src_app_language = "go" + generated_app_src_root_dir_name = "go_app_async_extension_python_app" + + replace_files_after_install_app = [ + "go_app_async_extension_python_app/manifest.json", + "go_app_async_extension_python_app/property.json", + ] + + replace_files_after_install_all = [ + "go_app_async_extension_python_app/ten_packages/extension/default_extension_python/extension.py", + "go_app_async_extension_python_app/bin", + ] + + if (ten_enable_package_manager) { + deps = [ + "//core/src/ten_manager", + "//packages/core_apps/default_app_go:upload_default_app_go_to_server", + "//packages/core_extensions/default_extension_go:upload_default_extension_go_to_server", + "//packages/core_extensions/default_extension_python:upload_default_extension_python_to_server", + "//packages/core_extensions/py_init_extension_cpp:upload_py_init_extension_cpp_to_server", + "//packages/example_extensions/pil_demo_python:upload_pil_demo_python_to_server", + "//packages/example_extensions/simple_echo_cpp:upload_simple_echo_cpp_to_server", + "//packages/example_extensions/simple_http_server_cpp:upload_simple_http_server_cpp_to_server", + ] + } +} + +ten_package_test_prepare_auxiliary_resources("go_app_async_extension_python_test_files") { + resources = [ + "//tests/ten_runtime/integration/common=>common", + "__init__.py", + "test_case.py", + ] + if (enable_sanitizer && !is_clang) { + resources += [ "//tests/ten_runtime/integration/tools/use_asan_lib_marker=>use_asan_lib_marker" ] + } +} + +group("go_app_async_extension_python") { + deps = [ + ":go_app_async_extension_python_app", + ":go_app_async_extension_python_test_files", + ] +} diff --git a/tests/ten_runtime/integration/python/go_app_async_extension_python/__init__.py b/tests/ten_runtime/integration/python/go_app_async_extension_python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/bin/bootstrap b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/bin/bootstrap new file mode 100755 index 0000000000..7ae31c9abb --- /dev/null +++ b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/bin/bootstrap @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +# Resolve the dependencies of the Python app and generate the 'merged_requirements.txt' file. +# get the result code of the following python script. +python3 ten_packages/system/ten_runtime_python/tools/deps_resolver.py -i https://pypi.tuna.tsinghua.edu.cn/simple/ +result_code=$? + +# If the Python script is executed failed, exit the script. +if [[ $result_code -ne 0 ]]; then + echo "Error: Failed to resolve the dependencies of the Python app. Cannot proceed to start." + exit $result_code +fi + +if [[ -f "merged_requirements.txt" ]]; then + echo "The 'merged_requirements.txt' file is generated successfully." + # Pip install the dependencies in the 'merged_requirements.txt' file. + pip install -r merged_requirements.txt +else + echo "No 'merged_requirements.txt' file is generated, because there are no dependencies." +fi diff --git a/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/manifest.json b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/manifest.json new file mode 100644 index 0000000000..c32778b6f5 --- /dev/null +++ b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/manifest.json @@ -0,0 +1,44 @@ +{ + "dependencies": [ + { + "type": "system", + "name": "ten_runtime", + "version": "0.3.0-alpha" + }, + { + "type": "system", + "name": "ten_runtime_python", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "default_extension_go", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_http_server_cpp", + "version": "0.1.0" + }, + { + "type": "extension", + "name": "pil_demo_python", + "version": "0.1.0" + }, + { + "type": "extension", + "name": "default_extension_python", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "py_init_extension_cpp", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "version": "0.1.0" + } + ] +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/property.json b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/property.json new file mode 100644 index 0000000000..3631a072e9 --- /dev/null +++ b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/property.json @@ -0,0 +1,92 @@ +{ + "_ten": { + "log_level": 2, + "predefined_graphs": [ + { + "name": "default", + "auto_start": true, + "nodes": [ + { + "type": "extension_group", + "name": "default_extension_group", + "addon": "default_extension_group" + }, + { + "type": "extension", + "name": "simple_http_server_cpp", + "addon": "simple_http_server_cpp", + "extension_group": "default_extension_group", + "property": { + "server_port": 8002 + } + }, + { + "type": "extension", + "name": "default_extension_python", + "addon": "default_extension_python", + "extension_group": "default_extension_group" + }, + { + "type": "extension", + "name": "default_extension_go", + "addon": "default_extension_go", + "extension_group": "default_extension_group" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "addon": "simple_echo_cpp", + "extension_group": "default_extension_group" + }, + { + "type": "extension", + "name": "pil_demo_python", + "addon": "pil_demo_python", + "extension_group": "default_extension_group" + } + ], + "connections": [ + { + "extension_group": "default_extension_group", + "extension": "simple_http_server_cpp", + "cmd": [ + { + "name": "test", + "dest": [ + { + "extension_group": "default_extension_group", + "extension": "default_extension_python" + } + ] + } + ] + }, + { + "extension_group": "default_extension_group", + "extension": "default_extension_python", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "default_extension_group", + "extension": "simple_echo_cpp" + } + ] + }, + { + "name": "greeting", + "dest": [ + { + "extension_group": "default_extension_group", + "extension": "default_extension_go" + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/ten_packages/extension/default_extension_python/extension.py b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/ten_packages/extension/default_extension_python/extension.py new file mode 100644 index 0000000000..f872b2e2a0 --- /dev/null +++ b/tests/ten_runtime/integration/python/go_app_async_extension_python/go_app_async_extension_python_app/ten_packages/extension/default_extension_python/extension.py @@ -0,0 +1,97 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# + +# import debugpy +# debugpy.listen(5678) +# debugpy.wait_for_client() + +import asyncio +from ten import ( + AsyncExtension, + AsyncTenEnv, + Cmd, + StatusCode, + CmdResult, +) + + +class DefaultExtension(AsyncExtension): + async def on_configure(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_init") + + ten_env.init_property_from_json('{"testKey": "testValue"}') + + await asyncio.sleep(0.5) + + ten_env.on_configure_done() + + async def on_start(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_start") + + await asyncio.sleep(0.5) + + ten_env.set_property_from_json("testKey2", '"testValue2"') + testValue = ten_env.get_property_to_json("testKey") + testValue2 = ten_env.get_property_to_json("testKey2") + ten_env.log_info(f"testValue: {testValue}, testValue2: {testValue2}") + + ten_env.on_start_done() + + async def on_stop(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_stop") + + await asyncio.sleep(0.5) + + ten_env.on_stop_done() + + async def on_deinit(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("on_deinit") + + await asyncio.sleep(0.5) + ten_env.on_deinit_done() + + async def greeting(self, ten_env: AsyncTenEnv) -> CmdResult: + await asyncio.sleep(1) + + new_cmd = Cmd.create("greeting") + return await ten_env.send_cmd( + new_cmd, + ) + + async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None: + cmd_json = cmd.to_json() + ten_env.log_debug("on_cmd: " + cmd_json) + + new_cmd = Cmd.create("hello") + new_cmd.set_property_from_json("test", '"testValue2"') + test_value = new_cmd.get_property_to_json("test") + ten_env.log_info(f"on_cmd test_value: {test_value}") + + await asyncio.sleep(0.5) + + result = await ten_env.send_cmd(new_cmd) + + statusCode = result.get_status_code() + detail = result.get_property_string("detail") + ten_env.log_info( + f"check_hello: status: {str(statusCode)}, detail: {detail}" + ) + + greeting_tasks = [self.greeting(ten_env) for _ in range(100)] + + results = await asyncio.gather(*greeting_tasks) + + for result in results: + statusCode = result.get_status_code() + if statusCode != StatusCode.OK: + ten_env.log_fatal(f"check_hello: status: {str(statusCode)}") + + respCmd = CmdResult.create(StatusCode.OK) + respCmd.set_property_string("detail", "received response") + ten_env.log_info("create respCmd") + + ten_env.return_result(respCmd, cmd) diff --git a/tests/ten_runtime/integration/python/go_app_async_extension_python/test_case.py b/tests/ten_runtime/integration/python/go_app_async_extension_python/test_case.py new file mode 100644 index 0000000000..0440d9f454 --- /dev/null +++ b/tests/ten_runtime/integration/python/go_app_async_extension_python/test_case.py @@ -0,0 +1,128 @@ +""" +Test go_app_async_extension_python. +""" + +import subprocess +import os +import sys +from sys import stdout +from .common import http + + +def http_request(): + return http.post( + "http://127.0.0.1:8002/", + { + "_ten": { + "name": "test", + }, + }, + ) + + +def test_go_app_async_extension_python(): + """Test client and app server.""" + base_path = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.join(base_path, "../../../../../") + + # Create virtual environment. + venv_dir = os.path.join(base_path, "venv") + subprocess.run([sys.executable, "-m", "venv", venv_dir]) + + my_env = os.environ.copy() + + # Set the required environment variables for the test. + my_env["PYTHONMALLOC"] = "malloc" + my_env["PYTHONDEVMODE"] = "1" + + # Launch virtual environment. + my_env["VIRTUAL_ENV"] = venv_dir + my_env["PATH"] = os.path.join(venv_dir, "bin") + os.pathsep + my_env["PATH"] + + if sys.platform == "win32": + print("test_go_app_async_extension_python doesn't support win32") + assert False + elif sys.platform == "darwin": + # client depends on some libraries in the TEN app. + my_env["DYLD_LIBRARY_PATH"] = os.path.join( + base_path, "go_app_async_extension_python_app/lib" + ) + else: + # client depends on some libraries in the TEN app. + my_env["LD_LIBRARY_PATH"] = os.path.join( + base_path, "go_app_async_extension_python_app/lib" + ) + + app_root_path = os.path.join(base_path, "go_app_async_extension_python_app") + + tman_install_cmd = [ + os.path.join(root_dir, "ten_manager/bin/tman"), + "--config-file", + os.path.join(root_dir, "tests/local_registry/config.json"), + "install", + ] + + tman_install_process = subprocess.Popen( + tman_install_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + tman_install_process.wait() + + bootstrap_cmd = os.path.join(base_path, "go_app_async_extension_python_app/bin/bootstrap") + + bootstrap_process = subprocess.Popen( + bootstrap_cmd, stdout=stdout, stderr=subprocess.STDOUT, env=my_env + ) + bootstrap_process.wait() + + if sys.platform == "linux": + if os.path.exists(os.path.join(base_path, "use_asan_lib_marker")): + libasan_path = os.path.join( + base_path, + "go_app_async_extension_python_app/ten_packages/system/ten_runtime/lib/libasan.so", + ) + + if os.path.exists(libasan_path): + my_env["LD_PRELOAD"] = libasan_path + + server_cmd = os.path.join(base_path, "go_app_async_extension_python_app/bin/start") + + server = subprocess.Popen( + server_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + + is_started = http.is_app_started("127.0.0.1", 8002, 30) + if not is_started: + print("The go_app_async_extension_python is not started after 30 seconds.") + + server.kill() + exit_code = server.wait() + print("The exit code of go_app_async_extension_python: ", exit_code) + + assert exit_code == 0 + assert 0 + + return + + try: + resp = http_request() + assert resp != 500 + print(resp) + + finally: + is_stopped = http.stop_app("127.0.0.1", 8002, 30) + if not is_stopped: + print("The go_app_async_extension_python can not stop after 30 seconds.") + server.kill() + + exit_code = server.wait() + print("The exit code of go_app_async_extension_python: ", exit_code) + + assert exit_code == 0 diff --git a/tests/ten_runtime/integration/python/two_async_exts_one_group_python/BUILD.gn b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/BUILD.gn new file mode 100644 index 0000000000..3918a98ecc --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/BUILD.gn @@ -0,0 +1,50 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import("//build/ten_runtime/feature/test.gni") +import("//build/ten_runtime/ten.gni") + +ten_package_test_prepare_app("two_async_exts_one_group_python_app") { + src_app = "default_app_python" + src_app_language = "python" + generated_app_src_root_dir_name = "two_async_exts_one_group_python_app" + + replace_files_after_install_app = [ + "two_async_exts_one_group_python_app/manifest.json", + "two_async_exts_one_group_python_app/property.json", + ] + + replace_files_after_install_all = [ "two_async_exts_one_group_python_app/ten_packages/extension/default_extension_python/extension.py" ] + + if (ten_enable_package_manager) { + deps = [ + "//core/src/ten_manager", + "//packages/core_apps/default_app_python:upload_default_app_python_to_server", + "//packages/core_extensions/default_extension_python:upload_default_extension_python_to_server", + "//packages/example_extensions/simple_echo_cpp:upload_simple_echo_cpp_to_server", + "//packages/example_extensions/simple_http_server_cpp:upload_simple_http_server_cpp_to_server", + ] + } +} + +ten_package_test_prepare_auxiliary_resources( + "two_async_exts_one_group_python_test_files") { + resources = [ + "//tests/ten_runtime/integration/common=>common", + "__init__.py", + "test_case.py", + ] + if (enable_sanitizer) { + resources += [ "//tests/ten_runtime/integration/tools/use_asan_lib_marker=>use_asan_lib_marker" ] + } +} + +group("two_async_exts_one_group_python") { + deps = [ + ":two_async_exts_one_group_python_app", + ":two_async_exts_one_group_python_test_files", + ] +} diff --git a/tests/ten_runtime/integration/python/two_async_exts_one_group_python/__init__.py b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/ten_runtime/integration/python/two_async_exts_one_group_python/test_case.py b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/test_case.py new file mode 100644 index 0000000000..61f10f485e --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/test_case.py @@ -0,0 +1,130 @@ +""" +Test two_async_exts_one_group_python. +""" + +import subprocess +import os +import sys +from sys import stdout +from .common import http + + +def http_request(): + return http.post( + "http://127.0.0.1:8002/", + { + "_ten": { + "name": "test", + }, + }, + ) + + +def test_two_async_exts_one_group_python(): + """Test client and app server.""" + base_path = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.join(base_path, "../../../../../") + + # Create virtual environment. + venv_dir = os.path.join(base_path, "venv") + subprocess.run([sys.executable, "-m", "venv", venv_dir]) + + my_env = os.environ.copy() + + # Set the required environment variables for the test. + my_env["PYTHONMALLOC"] = "malloc" + my_env["PYTHONDEVMODE"] = "1" + + # Launch virtual environment. + my_env["VIRTUAL_ENV"] = venv_dir + my_env["PATH"] = os.path.join(venv_dir, "bin") + os.pathsep + my_env["PATH"] + + if sys.platform == "win32": + print("test_two_async_exts_one_group_python doesn't support win32") + assert False + elif sys.platform == "darwin": + # client depends on some libraries in the TEN app. + my_env["DYLD_LIBRARY_PATH"] = os.path.join( + base_path, "two_async_exts_one_group_python_app/lib" + ) + else: + # client depends on some libraries in the TEN app. + my_env["LD_LIBRARY_PATH"] = os.path.join( + base_path, "two_async_exts_one_group_python_app/lib" + ) + + app_root_path = os.path.join(base_path, "two_async_exts_one_group_python_app") + + tman_install_cmd = [ + os.path.join(root_dir, "ten_manager/bin/tman"), + "--config-file", + os.path.join(root_dir, "tests/local_registry/config.json"), + "install", + ] + + tman_install_process = subprocess.Popen( + tman_install_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + tman_install_process.wait() + + bootstrap_cmd = os.path.join( + base_path, "two_async_exts_one_group_python_app/bin/bootstrap" + ) + + bootstrap_process = subprocess.Popen( + bootstrap_cmd, stdout=stdout, stderr=subprocess.STDOUT, env=my_env + ) + bootstrap_process.wait() + + if sys.platform == "linux": + if os.path.exists(os.path.join(base_path, "use_asan_lib_marker")): + libasan_path = os.path.join( + base_path, + "two_async_exts_one_group_python_app/ten_packages/system/ten_runtime/lib/libasan.so", + ) + + if os.path.exists(libasan_path): + my_env["LD_PRELOAD"] = libasan_path + + server_cmd = os.path.join(base_path, "two_async_exts_one_group_python_app/bin/start") + + server = subprocess.Popen( + server_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + + is_started = http.is_app_started("127.0.0.1", 8002, 30) + if not is_started: + print("The two_async_exts_one_group_python is not started after 30 seconds.") + + server.kill() + exit_code = server.wait() + print("The exit code of two_async_exts_one_group_python: ", exit_code) + + assert exit_code == 0 + assert 0 + + return + + try: + resp = http_request() + assert resp != 500 + print(resp) + + finally: + is_stopped = http.stop_app("127.0.0.1", 8002, 30) + if not is_stopped: + print("The two_async_exts_one_group_python can not stop after 30 seconds.") + server.kill() + + exit_code = server.wait() + print("The exit code of two_async_exts_one_group_python: ", exit_code) + + assert exit_code == 0 diff --git a/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/manifest.json b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/manifest.json new file mode 100644 index 0000000000..6d61860568 --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/manifest.json @@ -0,0 +1,24 @@ +{ + "dependencies": [ + { + "type": "system", + "name": "ten_runtime", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_http_server_cpp", + "version": "0.1.0" + }, + { + "type": "extension", + "name": "default_extension_python", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "version": "0.1.0" + } + ] +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/property.json b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/property.json new file mode 100644 index 0000000000..a02d5f9365 --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/property.json @@ -0,0 +1,97 @@ +{ + "_ten": { + "log_level": 2, + "predefined_graphs": [ + { + "name": "default", + "auto_start": true, + "nodes": [ + { + "type": "extension_group", + "name": "default_extension_group", + "addon": "default_extension_group" + }, + { + "type": "extension_group", + "name": "test", + "addon": "default_extension_group" + }, + { + "type": "extension", + "name": "simple_http_server_cpp", + "addon": "simple_http_server_cpp", + "extension_group": "default_extension_group", + "property": { + "server_port": 8002 + } + }, + { + "type": "extension", + "name": "default_extension_python", + "addon": "default_extension_python", + "extension_group": "test" + }, + { + "type": "extension", + "name": "default_extension_python2", + "addon": "default_extension_python", + "extension_group": "test" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "addon": "simple_echo_cpp", + "extension_group": "default_extension_group" + } + ], + "connections": [ + { + "extension_group": "default_extension_group", + "extension": "simple_http_server_cpp", + "cmd": [ + { + "name": "test", + "dest": [ + { + "extension_group": "test", + "extension": "default_extension_python" + } + ] + } + ] + }, + { + "extension_group": "test", + "extension": "default_extension_python", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "test", + "extension": "default_extension_python2" + } + ] + } + ] + }, + { + "extension_group": "test", + "extension": "default_extension_python2", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "default_extension_group", + "extension": "simple_echo_cpp" + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/ten_packages/extension/default_extension_python/extension.py b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/ten_packages/extension/default_extension_python/extension.py new file mode 100644 index 0000000000..e09d939aa8 --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_one_group_python/two_async_exts_one_group_python_app/ten_packages/extension/default_extension_python/extension.py @@ -0,0 +1,56 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import asyncio +from ten import ( + AsyncExtension, + AsyncTenEnv, + Cmd, +) + + +class DefaultExtension(AsyncExtension): + def __init__(self, name: str) -> None: + super().__init__(name) + self.name = name + + async def on_configure(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_configure_done() + + async def on_init(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_init_done() + + async def on_start(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.log_debug("on_start") + ten_env.on_start_done() + + async def on_deinit(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_deinit_done() + + await asyncio.sleep(1) + + async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None: + cmd_json = cmd.to_json() + ten_env.log_debug(f"DefaultExtension on_cmd: {cmd_json}") + + # Mock async operation, e.g. network, file I/O. + await asyncio.sleep(0.5) + + # Send a new command to other extensions and wait for the result. The + # result will be returned to the original sender. + new_cmd = Cmd.create("hello") + cmd_result = await ten_env.send_cmd(new_cmd) + ten_env.return_result(cmd_result, cmd) + + async def on_stop(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("DefaultExtension on_stop") + + await asyncio.sleep(0.5) + ten_env.on_stop_done() diff --git a/tests/ten_runtime/integration/python/two_async_exts_python/BUILD.gn b/tests/ten_runtime/integration/python/two_async_exts_python/BUILD.gn new file mode 100644 index 0000000000..5601b48624 --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_python/BUILD.gn @@ -0,0 +1,50 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import("//build/ten_runtime/feature/test.gni") +import("//build/ten_runtime/ten.gni") + +ten_package_test_prepare_app("two_async_exts_python_app") { + src_app = "default_app_python" + src_app_language = "python" + generated_app_src_root_dir_name = "two_async_exts_python_app" + + replace_files_after_install_app = [ + "two_async_exts_python_app/manifest.json", + "two_async_exts_python_app/property.json", + ] + + replace_files_after_install_all = [ "two_async_exts_python_app/ten_packages/extension/default_extension_python/extension.py" ] + + if (ten_enable_package_manager) { + deps = [ + "//core/src/ten_manager", + "//packages/core_apps/default_app_python:upload_default_app_python_to_server", + "//packages/core_extensions/default_extension_python:upload_default_extension_python_to_server", + "//packages/example_extensions/simple_echo_cpp:upload_simple_echo_cpp_to_server", + "//packages/example_extensions/simple_http_server_cpp:upload_simple_http_server_cpp_to_server", + ] + } +} + +ten_package_test_prepare_auxiliary_resources( + "two_async_exts_python_test_files") { + resources = [ + "//tests/ten_runtime/integration/common=>common", + "__init__.py", + "test_case.py", + ] + if (enable_sanitizer) { + resources += [ "//tests/ten_runtime/integration/tools/use_asan_lib_marker=>use_asan_lib_marker" ] + } +} + +group("two_async_exts_python") { + deps = [ + ":two_async_exts_python_app", + ":two_async_exts_python_test_files", + ] +} diff --git a/tests/ten_runtime/integration/python/two_async_exts_python/__init__.py b/tests/ten_runtime/integration/python/two_async_exts_python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/ten_runtime/integration/python/two_async_exts_python/test_case.py b/tests/ten_runtime/integration/python/two_async_exts_python/test_case.py new file mode 100644 index 0000000000..304dded44b --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_python/test_case.py @@ -0,0 +1,130 @@ +""" +Test two_async_exts_python. +""" + +import subprocess +import os +import sys +from sys import stdout +from .common import http + + +def http_request(): + return http.post( + "http://127.0.0.1:8002/", + { + "_ten": { + "name": "test", + }, + }, + ) + + +def test_two_async_exts_python(): + """Test client and app server.""" + base_path = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.join(base_path, "../../../../../") + + # Create virtual environment. + venv_dir = os.path.join(base_path, "venv") + subprocess.run([sys.executable, "-m", "venv", venv_dir]) + + my_env = os.environ.copy() + + # Set the required environment variables for the test. + my_env["PYTHONMALLOC"] = "malloc" + my_env["PYTHONDEVMODE"] = "1" + + # Launch virtual environment. + my_env["VIRTUAL_ENV"] = venv_dir + my_env["PATH"] = os.path.join(venv_dir, "bin") + os.pathsep + my_env["PATH"] + + if sys.platform == "win32": + print("test_two_async_exts_python doesn't support win32") + assert False + elif sys.platform == "darwin": + # client depends on some libraries in the TEN app. + my_env["DYLD_LIBRARY_PATH"] = os.path.join( + base_path, "two_async_exts_python_app/lib" + ) + else: + # client depends on some libraries in the TEN app. + my_env["LD_LIBRARY_PATH"] = os.path.join( + base_path, "two_async_exts_python_app/lib" + ) + + app_root_path = os.path.join(base_path, "two_async_exts_python_app") + + tman_install_cmd = [ + os.path.join(root_dir, "ten_manager/bin/tman"), + "--config-file", + os.path.join(root_dir, "tests/local_registry/config.json"), + "install", + ] + + tman_install_process = subprocess.Popen( + tman_install_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + tman_install_process.wait() + + bootstrap_cmd = os.path.join( + base_path, "two_async_exts_python_app/bin/bootstrap" + ) + + bootstrap_process = subprocess.Popen( + bootstrap_cmd, stdout=stdout, stderr=subprocess.STDOUT, env=my_env + ) + bootstrap_process.wait() + + if sys.platform == "linux": + if os.path.exists(os.path.join(base_path, "use_asan_lib_marker")): + libasan_path = os.path.join( + base_path, + "two_async_exts_python_app/ten_packages/system/ten_runtime/lib/libasan.so", + ) + + if os.path.exists(libasan_path): + my_env["LD_PRELOAD"] = libasan_path + + server_cmd = os.path.join(base_path, "two_async_exts_python_app/bin/start") + + server = subprocess.Popen( + server_cmd, + stdout=stdout, + stderr=subprocess.STDOUT, + env=my_env, + cwd=app_root_path, + ) + + is_started = http.is_app_started("127.0.0.1", 8002, 30) + if not is_started: + print("The two_async_exts_python is not started after 30 seconds.") + + server.kill() + exit_code = server.wait() + print("The exit code of two_async_exts_python: ", exit_code) + + assert exit_code == 0 + assert 0 + + return + + try: + resp = http_request() + assert resp != 500 + print(resp) + + finally: + is_stopped = http.stop_app("127.0.0.1", 8002, 30) + if not is_stopped: + print("The two_async_exts_python can not stop after 30 seconds.") + server.kill() + + exit_code = server.wait() + print("The exit code of two_async_exts_python: ", exit_code) + + assert exit_code == 0 diff --git a/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/manifest.json b/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/manifest.json new file mode 100644 index 0000000000..6d61860568 --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/manifest.json @@ -0,0 +1,24 @@ +{ + "dependencies": [ + { + "type": "system", + "name": "ten_runtime", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_http_server_cpp", + "version": "0.1.0" + }, + { + "type": "extension", + "name": "default_extension_python", + "version": "0.3.0-alpha" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "version": "0.1.0" + } + ] +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/property.json b/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/property.json new file mode 100644 index 0000000000..12cd22dfc1 --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/property.json @@ -0,0 +1,87 @@ +{ + "_ten": { + "log_level": 2, + "predefined_graphs": [ + { + "name": "default", + "auto_start": true, + "nodes": [ + { + "type": "extension", + "name": "simple_http_server_cpp", + "addon": "simple_http_server_cpp", + "extension_group": "default_extension_group", + "property": { + "server_port": 8002 + } + }, + { + "type": "extension", + "name": "default_extension_python", + "addon": "default_extension_python", + "extension_group": "test" + }, + { + "type": "extension", + "name": "default_extension_python2", + "addon": "default_extension_python", + "extension_group": "test2" + }, + { + "type": "extension", + "name": "simple_echo_cpp", + "addon": "simple_echo_cpp", + "extension_group": "default_extension_group" + } + ], + "connections": [ + { + "extension_group": "default_extension_group", + "extension": "simple_http_server_cpp", + "cmd": [ + { + "name": "test", + "dest": [ + { + "extension_group": "test", + "extension": "default_extension_python" + } + ] + } + ] + }, + { + "extension_group": "test", + "extension": "default_extension_python", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "test2", + "extension": "default_extension_python2" + } + ] + } + ] + }, + { + "extension_group": "test2", + "extension": "default_extension_python2", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "default_extension_group", + "extension": "simple_echo_cpp" + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/ten_packages/extension/default_extension_python/extension.py b/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/ten_packages/extension/default_extension_python/extension.py new file mode 100644 index 0000000000..e09d939aa8 --- /dev/null +++ b/tests/ten_runtime/integration/python/two_async_exts_python/two_async_exts_python_app/ten_packages/extension/default_extension_python/extension.py @@ -0,0 +1,56 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import asyncio +from ten import ( + AsyncExtension, + AsyncTenEnv, + Cmd, +) + + +class DefaultExtension(AsyncExtension): + def __init__(self, name: str) -> None: + super().__init__(name) + self.name = name + + async def on_configure(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_configure_done() + + async def on_init(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_init_done() + + async def on_start(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.log_debug("on_start") + ten_env.on_start_done() + + async def on_deinit(self, ten_env: AsyncTenEnv) -> None: + await asyncio.sleep(0.5) + ten_env.on_deinit_done() + + await asyncio.sleep(1) + + async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None: + cmd_json = cmd.to_json() + ten_env.log_debug(f"DefaultExtension on_cmd: {cmd_json}") + + # Mock async operation, e.g. network, file I/O. + await asyncio.sleep(0.5) + + # Send a new command to other extensions and wait for the result. The + # result will be returned to the original sender. + new_cmd = Cmd.create("hello") + cmd_result = await ten_env.send_cmd(new_cmd) + ten_env.return_result(cmd_result, cmd) + + async def on_stop(self, ten_env: AsyncTenEnv) -> None: + ten_env.log_debug("DefaultExtension on_stop") + + await asyncio.sleep(0.5) + ten_env.on_stop_done()