remote-exec-grpc 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. remote_exec_grpc-0.2.2/.github/workflows/ci.yml +49 -0
  2. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/PKG-INFO +1 -1
  3. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/pyproject.toml +1 -1
  4. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/executor.py +32 -2
  5. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/uv.lock +1 -1
  6. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/.gitignore +0 -0
  7. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/Makefile +0 -0
  8. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/README.md +0 -0
  9. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/README_CN.md +0 -0
  10. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/docs/requirements.md +0 -0
  11. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/examples/config.yaml +0 -0
  12. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/examples/remote-exec-server.service +0 -0
  13. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/examples/server.yaml +0 -0
  14. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/examples/tools.yaml +0 -0
  15. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/proto/remote_exec/v1/remote_exec.proto +0 -0
  16. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/scripts/gen_proto.sh +0 -0
  17. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/__init__.py +0 -0
  18. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/claude_skill.md +0 -0
  19. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/client.py +0 -0
  20. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/client_cli.py +0 -0
  21. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/cmdlog.py +0 -0
  22. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/config.py +0 -0
  23. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/policy.py +0 -0
  24. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/security.py +0 -0
  25. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/server.py +0 -0
  26. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/server_cli.py +0 -0
  27. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/skill.py +0 -0
  28. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/v1/__init__.py +0 -0
  29. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/v1/remote_exec_pb2.py +0 -0
  30. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/v1/remote_exec_pb2.pyi +0 -0
  31. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/src/remote_exec/v1/remote_exec_pb2_grpc.py +0 -0
  32. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/conftest.py +0 -0
  33. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_cli.py +0 -0
  34. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_cmdlog.py +0 -0
  35. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_config.py +0 -0
  36. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_executor.py +0 -0
  37. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_grpc.py +0 -0
  38. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_policy.py +0 -0
  39. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_skill.py +0 -0
  40. {remote_exec_grpc-0.2.1 → remote_exec_grpc-0.2.2}/tests/test_token.py +0 -0
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+
7
+ concurrency:
8
+ group: ci-${{ github.ref }}
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ test:
13
+ name: test (Python ${{ matrix.python-version }})
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 15
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.11", "3.12", "3.13"]
20
+ steps:
21
+ - uses: actions/checkout@v5
22
+
23
+ - name: Set up uv
24
+ uses: astral-sh/setup-uv@v6
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ enable-cache: true
28
+
29
+ - name: Install dependencies
30
+ run: uv sync --locked
31
+
32
+ - name: Run tests
33
+ run: uv run pytest -q
34
+
35
+ build:
36
+ name: build package
37
+ runs-on: ubuntu-latest
38
+ timeout-minutes: 10
39
+ steps:
40
+ - uses: actions/checkout@v5
41
+
42
+ - name: Set up uv
43
+ uses: astral-sh/setup-uv@v6
44
+
45
+ - name: Build sdist and wheel
46
+ run: uv build
47
+
48
+ - name: Check package metadata
49
+ run: uvx twine check dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: remote-exec-grpc
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Lightweight gRPC remote command execution with real-time stdout/stderr streaming
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "remote-exec-grpc"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Lightweight gRPC remote command execution with real-time stdout/stderr streaming"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -122,6 +122,35 @@ async def terminate_process_group(pgid: int, grace: float = DEFAULT_KILL_GRACE)
122
122
  _signal_process_group(pgid, signal.SIGKILL)
123
123
 
124
124
 
125
+ class _ExitNotifyingProtocol(asyncio.subprocess.SubprocessStreamProtocol):
126
+ """Stream protocol that resolves ``exited`` as soon as the process exits.
127
+
128
+ Before Python 3.13, ``Process.wait()`` only returns once the process has
129
+ exited *and* all its pipes are closed. A background child that inherited
130
+ stdout (``sleep 300 &``) would therefore block ``wait()`` until it exits,
131
+ so we could never notice the main process exiting and clean up its group.
132
+ """
133
+
134
+ def __init__(self, limit: int, loop: asyncio.AbstractEventLoop):
135
+ super().__init__(limit=limit, loop=loop)
136
+ self.exited: asyncio.Future[int] = loop.create_future()
137
+
138
+ def process_exited(self) -> None:
139
+ # Read the code first: the base class may drop _transport (3.13+).
140
+ returncode = self._transport.get_returncode()
141
+ super().process_exited()
142
+ if not self.exited.done():
143
+ self.exited.set_result(returncode)
144
+
145
+
146
+ async def _spawn(program: str, *args: str, **kwds) -> tuple[asyncio.subprocess.Process, asyncio.Future[int]]:
147
+ """Like ``asyncio.create_subprocess_exec`` but also returns an exit future."""
148
+ loop = asyncio.get_running_loop()
149
+ protocol_factory = lambda: _ExitNotifyingProtocol(limit=2**16, loop=loop) # noqa: E731
150
+ transport, protocol = await loop.subprocess_exec(protocol_factory, program, *args, **kwds)
151
+ return asyncio.subprocess.Process(transport, protocol, loop), protocol.exited
152
+
153
+
125
154
  class Executor:
126
155
  """Runs commands; tracks background cleanup tasks so shutdown can await them."""
127
156
 
@@ -151,7 +180,7 @@ class Executor:
151
180
  start_time = utcnow()
152
181
 
153
182
  try:
154
- proc = await asyncio.create_subprocess_exec(
183
+ proc, exited = await _spawn(
155
184
  self.shell,
156
185
  "-c",
157
186
  spec.command,
@@ -187,7 +216,8 @@ class Executor:
187
216
  pass
188
217
 
189
218
  async def wait_and_reap() -> int:
190
- rc = await proc.wait()
219
+ # Not proc.wait(): before 3.13 it also waits for the pipes to close.
220
+ rc = await exited
191
221
  # Kill whatever the command left behind in its process group.
192
222
  await terminate_process_group(pgid, self.kill_grace)
193
223
  return rc
@@ -272,7 +272,7 @@ wheels = [
272
272
 
273
273
  [[package]]
274
274
  name = "remote-exec-grpc"
275
- version = "0.2.1"
275
+ version = "0.2.2"
276
276
  source = { editable = "." }
277
277
  dependencies = [
278
278
  { name = "grpcio" },