diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py index 397c97699..1898b4f80 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py @@ -18,6 +18,7 @@ from ..agent_env_environment import OP_ADD_DATA, OP_GET_DATA, OP_RESET_DATA from ..transfers import WriteNamespaceGrant from ..types import METHOD_ADD, METHOD_RESET, FilePart, uploaded_object_path +from .app_runtime import paused_app from .dataplane import data_files, file_parts, part_bytes if TYPE_CHECKING: @@ -131,7 +132,7 @@ def _served(self, op: str) -> Optional[Callable]: if not self.paths: return None return {OP_ADD_DATA: functools.partial(add, paths=self.paths), - OP_GET_DATA: functools.partial(get, self.paths, self.runtime.paused), + OP_GET_DATA: functools.partial(get, self.paths), OP_RESET_DATA: functools.partial(reset, self.paths)}[op] async def _within(self, op: str, served: Callable, args: tuple, answer: asyncio.Future) -> Any: @@ -177,13 +178,12 @@ async def add(parts: list, paths: list[str]) -> None: await in_thread(write_within, Path.home(), files, paths) -async def get(paths: list[str], paused: Callable[[], contextlib.AbstractContextManager], - write_namespace: Optional[WriteNamespaceGrant]) -> list: - """A part for each file under ``paths``, copied with the app ``paused``, so they are of one moment, and sent from - the copy once it runs again.""" +async def get(paths: list[str], write_namespace: Optional[WriteNamespaceGrant]) -> list: + """A part for each file under ``paths``, copied with the app this server launched paused (``paused_app``), so they + are of one moment, and sent from the copy once it runs again; an app's own data/get hook calls it so too.""" INITIAL.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=INITIAL.parent) as staging: - with paused(): + with paused_app(): await in_thread(_stage, Path.home(), paths, Path(staging)) return await file_parts(Path(staging), paths, write_namespace) diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py index e1beac1fa..bf125781c 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py @@ -139,10 +139,8 @@ def running(self) -> bool: return _running() is not None def paused(self) -> contextlib.AbstractContextManager: - """The app this server launched last, every process of it, paused while the block runs, so a copy of its files - taken meanwhile is of one moment.""" - pid = _running() - return session.paused(pid) if pid is not None else contextlib.nullcontext() + """The app this server launched last paused while the block runs (``paused_app``).""" + return paused_app() def exited(self) -> bool: """Log that the app this server launched last ended on its own, with its exit code or the signal that ended @@ -242,6 +240,13 @@ def _running() -> Optional[int]: return pid if pid is not None and session.alive(pid) else None +def paused_app() -> contextlib.AbstractContextManager: + """The app this server launched last, every process of it, paused while the block runs, so a copy of its files + taken meanwhile is of one moment; nothing when it isn't running.""" + pid = _running() + return session.paused(pid) if pid is not None else contextlib.nullcontext() + + async def _bounded(name: str, check: Awaitable[Optional[str]]) -> Optional[str]: """What ``check`` finds wrong, within ``CHECK_TIMEOUT_S``; a check that runs past it or raises says so.""" try: diff --git a/packages/agentenv-protocol/tests/test_computer_app_data.py b/packages/agentenv-protocol/tests/test_computer_app_data.py index 99e18ec0c..b4638b324 100644 --- a/packages/agentenv-protocol/tests/test_computer_app_data.py +++ b/packages/agentenv-protocol/tests/test_computer_app_data.py @@ -138,6 +138,19 @@ async def test_an_app_with_only_paths_loads_exports_and_resets_them_as_installed assert reset == {"Mail/inbox.txt": b"installed"} +@pytest.mark.asyncio +async def test_get_takes_paths_and_a_write_namespace_and_pauses_the_running_app_as_exported_apps_call_it(home, + desktop): + (home / "Mail").mkdir() + (home / "Mail/inbox.txt").write_text("kept") + await PathsMail()._computer_app.launch_app() + + parts = await app_data.get(["Mail"], None) + + assert [await part_bytes(part) for part in parts] == [b"kept"] + assert desktop == [("launch", 4242), ("pause", 4242), ("continue", 4242)] + + @pytest.mark.asyncio async def test_a_file_under_none_of_its_paths_or_through_a_link_out_of_them_is_refused_and_nothing_is_written(home): (home / "Documents").mkdir()