dcc-mcp-renderdoc 0.3.1__tar.gz → 0.3.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.
- dcc_mcp_renderdoc-0.3.2/.release-please-manifest.json +2 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/CHANGELOG.md +7 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/PKG-INFO +1 -1
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/pyproject.toml +1 -1
- dcc_mcp_renderdoc-0.3.2/src/dcc_mcp_renderdoc/__version__.py +3 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/runtime.py +114 -28
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +3 -1
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +1 -1
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/test_runtime.py +177 -15
- dcc_mcp_renderdoc-0.3.1/.release-please-manifest.json +0 -2
- dcc_mcp_renderdoc-0.3.1/src/dcc_mcp_renderdoc/__version__.py +0 -3
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/.gitignore +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/LICENSE +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/README.md +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/release-please-config.json +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/server.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_process.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/live_renderdoc_smoke.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/renderdoc_smoke.c +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/test_download.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/test_package.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tools/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tools/download_latest_renderdoc.py +0 -0
- {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tools/lint_skills.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.2](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.1...v0.3.2) (2026-07-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* keep capture controller alive through trigger ([#11](https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues/11)) ([1fb7bed](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/1fb7bed40dfb027b148a14cefd01c53028891d56))
|
|
9
|
+
|
|
3
10
|
## [0.3.1](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.0...v0.3.1) (2026-07-22)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcc-mcp-renderdoc
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: RenderDoc capture and replay automation skills for DCC-MCP
|
|
5
5
|
Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-renderdoc
|
|
6
6
|
Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-renderdoc
|
|
@@ -8,6 +8,7 @@ import shutil
|
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
10
|
import tempfile
|
|
11
|
+
import threading
|
|
11
12
|
import time
|
|
12
13
|
import xml.etree.ElementTree as ET
|
|
13
14
|
from collections import Counter
|
|
@@ -19,6 +20,81 @@ class RenderDocError(RuntimeError):
|
|
|
19
20
|
"""Raised when a RenderDoc operation cannot satisfy its contract."""
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
class _CaptureController:
|
|
24
|
+
def __init__(self, process: Any, stdout: list[str], stderr: list[str], readers: list[Any]):
|
|
25
|
+
self.process = process
|
|
26
|
+
self.stdout = stdout
|
|
27
|
+
self.stderr = stderr
|
|
28
|
+
self.readers = readers
|
|
29
|
+
|
|
30
|
+
def output(self) -> tuple[str, str]:
|
|
31
|
+
return "".join(self.stdout), "".join(self.stderr)
|
|
32
|
+
|
|
33
|
+
def close(self) -> None:
|
|
34
|
+
if self.process.poll() is None:
|
|
35
|
+
self.process.terminate()
|
|
36
|
+
try:
|
|
37
|
+
self.process.wait(timeout=5)
|
|
38
|
+
except subprocess.TimeoutExpired:
|
|
39
|
+
self.process.kill()
|
|
40
|
+
self.process.wait(timeout=5)
|
|
41
|
+
for reader in self.readers:
|
|
42
|
+
reader.join(timeout=1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _start_capture_controller(
|
|
46
|
+
arguments: Sequence[str], *, timeout_secs: int, command: Optional[str]
|
|
47
|
+
) -> _CaptureController:
|
|
48
|
+
executable = resolve_renderdoccmd(command)
|
|
49
|
+
try:
|
|
50
|
+
process = subprocess.Popen(
|
|
51
|
+
[str(executable), *arguments],
|
|
52
|
+
stdout=subprocess.PIPE,
|
|
53
|
+
stderr=subprocess.PIPE,
|
|
54
|
+
text=True,
|
|
55
|
+
shell=False,
|
|
56
|
+
)
|
|
57
|
+
except OSError as exc:
|
|
58
|
+
raise RenderDocError(f"Could not start RenderDoc: {exc}") from exc
|
|
59
|
+
|
|
60
|
+
stdout: list[str] = []
|
|
61
|
+
stderr: list[str] = []
|
|
62
|
+
|
|
63
|
+
def read_stream(stream: Any, output: list[str]) -> None:
|
|
64
|
+
for chunk in iter(lambda: stream.read(1), ""):
|
|
65
|
+
output.append(chunk)
|
|
66
|
+
|
|
67
|
+
readers = [
|
|
68
|
+
threading.Thread(target=read_stream, args=(process.stdout, stdout), daemon=True),
|
|
69
|
+
threading.Thread(target=read_stream, args=(process.stderr, stderr), daemon=True),
|
|
70
|
+
]
|
|
71
|
+
for reader in readers:
|
|
72
|
+
reader.start()
|
|
73
|
+
controller = _CaptureController(process, stdout, stderr, readers)
|
|
74
|
+
deadline = time.monotonic() + timeout_secs
|
|
75
|
+
while True:
|
|
76
|
+
combined = "".join(stdout + stderr)
|
|
77
|
+
launched = re.search(r"Launched as ID (\d+)", combined)
|
|
78
|
+
returncode = process.poll()
|
|
79
|
+
if launched is not None:
|
|
80
|
+
launched_id = int(launched.group(1))
|
|
81
|
+
if returncode is None or returncode in (0, launched_id):
|
|
82
|
+
return controller
|
|
83
|
+
if returncode is not None:
|
|
84
|
+
controller.close()
|
|
85
|
+
detail = "".join(controller.output()).strip()[-2000:] or "unknown error"
|
|
86
|
+
raise RenderDocError(f"RenderDoc exited with code {returncode}: {detail}")
|
|
87
|
+
if time.monotonic() >= deadline:
|
|
88
|
+
controller.close()
|
|
89
|
+
detail = "".join(controller.output()).strip()[-2000:]
|
|
90
|
+
suffix = f": {detail}" if detail else ""
|
|
91
|
+
raise RenderDocError(
|
|
92
|
+
f"RenderDoc command timed out waiting for {arguments[0]} readiness after "
|
|
93
|
+
f"{timeout_secs}s{suffix}"
|
|
94
|
+
)
|
|
95
|
+
time.sleep(0.01)
|
|
96
|
+
|
|
97
|
+
|
|
22
98
|
def resolve_renderdoccmd(explicit: Optional[str] = None) -> Path:
|
|
23
99
|
"""Resolve an executable RenderDoc CLI without invoking a shell."""
|
|
24
100
|
candidate = explicit or os.environ.get("DCC_MCP_RENDERDOC_CMD")
|
|
@@ -115,23 +191,33 @@ def capture_program(
|
|
|
115
191
|
if hook_children:
|
|
116
192
|
cli_args.append("--opt-hook-children")
|
|
117
193
|
cli_args.extend([str(target), *(str(value) for value in arguments or [])])
|
|
118
|
-
result = _run(
|
|
119
|
-
cli_args,
|
|
120
|
-
timeout_secs=timeout_secs,
|
|
121
|
-
command=command,
|
|
122
|
-
accept_launched_id=not effective_wait_for_exit,
|
|
123
|
-
)
|
|
124
194
|
target_pid: Optional[int] = None
|
|
125
195
|
if trigger_after_secs is not None:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
process_name,
|
|
131
|
-
visible_before_launch,
|
|
132
|
-
capture_wait_secs,
|
|
196
|
+
controller = _start_capture_controller(
|
|
197
|
+
cli_args,
|
|
198
|
+
timeout_secs=timeout_secs,
|
|
199
|
+
command=command,
|
|
133
200
|
)
|
|
201
|
+
try:
|
|
202
|
+
time.sleep(max(0.0, trigger_after_secs))
|
|
203
|
+
target_pid, focused, captures = _wait_for_triggered_capture(
|
|
204
|
+
output.parent,
|
|
205
|
+
before,
|
|
206
|
+
process_name,
|
|
207
|
+
visible_before_launch,
|
|
208
|
+
capture_wait_secs,
|
|
209
|
+
)
|
|
210
|
+
finally:
|
|
211
|
+
controller.close()
|
|
212
|
+
stdout, stderr = controller.output()
|
|
134
213
|
else:
|
|
214
|
+
result = _run(
|
|
215
|
+
cli_args,
|
|
216
|
+
timeout_secs=timeout_secs,
|
|
217
|
+
command=command,
|
|
218
|
+
accept_launched_id=not effective_wait_for_exit,
|
|
219
|
+
)
|
|
220
|
+
stdout, stderr = result.stdout, result.stderr
|
|
135
221
|
focused = False
|
|
136
222
|
captures = _new_captures(output.parent, before)
|
|
137
223
|
injection_error: Optional[str] = None
|
|
@@ -157,9 +243,7 @@ def capture_program(
|
|
|
157
243
|
except RenderDocError as exc:
|
|
158
244
|
injection_error = str(exc)
|
|
159
245
|
if not captures:
|
|
160
|
-
output_detail = "\n".join(
|
|
161
|
-
part.strip() for part in (result.stdout, result.stderr) if part.strip()
|
|
162
|
-
)[-2000:]
|
|
246
|
+
output_detail = "\n".join(part.strip() for part in (stdout, stderr) if part.strip())[-2000:]
|
|
163
247
|
diagnostics = _capture_failure_diagnostics(
|
|
164
248
|
process_id=target_pid,
|
|
165
249
|
process_name=trigger_process_name or target.name,
|
|
@@ -175,7 +259,7 @@ def capture_program(
|
|
|
175
259
|
"target": str(target),
|
|
176
260
|
"captures": [str(path) for path in captures],
|
|
177
261
|
"focused_target_window": focused,
|
|
178
|
-
"stdout":
|
|
262
|
+
"stdout": stdout.strip(),
|
|
179
263
|
}
|
|
180
264
|
|
|
181
265
|
|
|
@@ -204,34 +288,36 @@ def capture_process(
|
|
|
204
288
|
cli_args.extend(["--working-dir", str(cwd)])
|
|
205
289
|
if api_validation:
|
|
206
290
|
cli_args.append("--opt-api-validation")
|
|
207
|
-
|
|
291
|
+
controller = _start_capture_controller(
|
|
208
292
|
cli_args,
|
|
209
293
|
timeout_secs=timeout_secs,
|
|
210
294
|
command=command,
|
|
211
|
-
accept_launched_id=True,
|
|
212
295
|
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
296
|
+
try:
|
|
297
|
+
time.sleep(max(0.0, trigger_after_secs))
|
|
298
|
+
focused = _trigger_capture_hotkey(process_id)
|
|
299
|
+
captures = _wait_for_captures(output.parent, before, capture_wait_secs)
|
|
300
|
+
finally:
|
|
301
|
+
controller.close()
|
|
302
|
+
stdout, stderr = controller.output()
|
|
216
303
|
if not captures:
|
|
217
|
-
output_detail = "\n".join(
|
|
218
|
-
part.strip() for part in (result.stdout, result.stderr) if part.strip()
|
|
219
|
-
)[-2000:]
|
|
304
|
+
output_detail = "\n".join(part.strip() for part in (stdout, stderr) if part.strip())[-2000:]
|
|
220
305
|
diagnostics = _capture_failure_diagnostics(
|
|
221
306
|
process_id=process_id,
|
|
222
307
|
process_name=None,
|
|
223
308
|
focused=focused,
|
|
224
309
|
)
|
|
225
310
|
raise RenderDocError(
|
|
226
|
-
"No .rdc capture appeared after injection and F12
|
|
227
|
-
|
|
311
|
+
"No .rdc capture appeared after injection and F12. Inject before the target creates "
|
|
312
|
+
"its graphics device; for an already initialized game, relaunch it with "
|
|
313
|
+
f"capture_program. {diagnostics}. "
|
|
228
314
|
f"RenderDoc output: {output_detail}"
|
|
229
315
|
)
|
|
230
316
|
return {
|
|
231
317
|
"process_id": process_id,
|
|
232
318
|
"captures": [str(path) for path in captures],
|
|
233
319
|
"focused_target_window": focused,
|
|
234
|
-
"stdout":
|
|
320
|
+
"stdout": stdout.strip(),
|
|
235
321
|
}
|
|
236
322
|
|
|
237
323
|
|
|
@@ -27,4 +27,6 @@ focus before F12.
|
|
|
27
27
|
|
|
28
28
|
Use `capture_process` when Steam or another platform client must launch the target first. It
|
|
29
29
|
injects into the explicit PID, focuses that visible window, triggers F12 after the requested delay,
|
|
30
|
-
and waits for the resulting capture.
|
|
30
|
+
and waits for the resulting capture. Inject before the target creates its graphics device; late
|
|
31
|
+
injection cannot recover resources that RenderDoc did not observe. Prefer `capture_program` when
|
|
32
|
+
the executable can be launched directly. Neither tool terminates the target process.
|
|
@@ -46,7 +46,7 @@ tools:
|
|
|
46
46
|
on-success: [renderdoc_analysis__inspect_capture, renderdoc_analysis__export_thumbnail]
|
|
47
47
|
on-failure: [dcc_diagnostics__audit_log]
|
|
48
48
|
- name: capture_process
|
|
49
|
-
description: Inject RenderDoc into one
|
|
49
|
+
description: Inject RenderDoc into one Windows process before it creates its graphics device, focus its visible window, trigger F12 after a delay, and return the new .rdc file. Use when a game must be launched by its platform client. Prefer capture_program for an already initialized game; not for hidden services or process termination.
|
|
50
50
|
input_schema:
|
|
51
51
|
type: object
|
|
52
52
|
required: [process_id, output_template]
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import io
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from subprocess import CompletedProcess
|
|
3
|
+
from subprocess import CompletedProcess, TimeoutExpired
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
7
|
from dcc_mcp_renderdoc import runtime
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
class StubController:
|
|
11
|
+
def __init__(self, stdout="Launched as ID 123", stderr=""):
|
|
12
|
+
self.stdout = stdout
|
|
13
|
+
self.stderr = stderr
|
|
14
|
+
|
|
15
|
+
def output(self):
|
|
16
|
+
return self.stdout, self.stderr
|
|
17
|
+
|
|
18
|
+
def close(self):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
9
22
|
def test_parse_capture_xml_summarizes_stable_fields(tmp_path: Path):
|
|
10
23
|
xml_file = tmp_path / "capture.xml"
|
|
11
24
|
xml_file.write_text(
|
|
@@ -76,10 +89,9 @@ def test_capture_process_injects_triggers_and_reports_capture(tmp_path, monkeypa
|
|
|
76
89
|
|
|
77
90
|
def fake_run(arguments, **kwargs):
|
|
78
91
|
observed["arguments"] = arguments
|
|
79
|
-
|
|
80
|
-
return CompletedProcess(arguments, 456, "Launched as ID 456", "")
|
|
92
|
+
return StubController("Launched as ID 456")
|
|
81
93
|
|
|
82
|
-
monkeypatch.setattr(runtime, "
|
|
94
|
+
monkeypatch.setattr(runtime, "_start_capture_controller", fake_run)
|
|
83
95
|
monkeypatch.setattr(runtime, "_trigger_capture_hotkey", lambda pid: pid == 42)
|
|
84
96
|
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
85
97
|
monkeypatch.setattr(
|
|
@@ -91,18 +103,168 @@ def test_capture_process_injects_triggers_and_reports_capture(tmp_path, monkeypa
|
|
|
91
103
|
result = runtime.capture_process(42, str(tmp_path / "capture"))
|
|
92
104
|
|
|
93
105
|
assert observed["arguments"][:2] == ["inject", "--PID=42"]
|
|
94
|
-
assert observed["accept_launched_id"] is True
|
|
95
106
|
assert result["focused_target_window"] is True
|
|
96
107
|
|
|
97
108
|
|
|
109
|
+
def test_capture_process_triggers_while_injector_is_running_and_only_stops_injector(
|
|
110
|
+
tmp_path, monkeypatch
|
|
111
|
+
):
|
|
112
|
+
started = {}
|
|
113
|
+
target = {"running": True}
|
|
114
|
+
command = tmp_path / "renderdoccmd.exe"
|
|
115
|
+
command.touch()
|
|
116
|
+
|
|
117
|
+
class Injector:
|
|
118
|
+
def __init__(self, arguments, **kwargs):
|
|
119
|
+
started.update(arguments=arguments, kwargs=kwargs, process=self)
|
|
120
|
+
self.stdout = io.StringIO("Launched as ID 42\n")
|
|
121
|
+
self.stderr = io.StringIO("")
|
|
122
|
+
self.returncode = None
|
|
123
|
+
self.terminated = False
|
|
124
|
+
self.killed = False
|
|
125
|
+
|
|
126
|
+
def poll(self):
|
|
127
|
+
return self.returncode
|
|
128
|
+
|
|
129
|
+
def terminate(self):
|
|
130
|
+
self.terminated = True
|
|
131
|
+
self.returncode = 0
|
|
132
|
+
|
|
133
|
+
def kill(self):
|
|
134
|
+
self.killed = True
|
|
135
|
+
self.returncode = -9
|
|
136
|
+
|
|
137
|
+
def wait(self, timeout=None):
|
|
138
|
+
if self.returncode is None:
|
|
139
|
+
raise TimeoutExpired(self.args, timeout)
|
|
140
|
+
return self.returncode
|
|
141
|
+
|
|
142
|
+
monkeypatch.setattr(runtime.subprocess, "Popen", Injector)
|
|
143
|
+
monkeypatch.setattr(runtime, "_trigger_capture_hotkey", lambda process_id: process_id == 42)
|
|
144
|
+
monkeypatch.setattr(
|
|
145
|
+
runtime,
|
|
146
|
+
"_wait_for_captures",
|
|
147
|
+
lambda directory, before, timeout: [tmp_path / "capture_frame1.rdc"],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
result = runtime.capture_process(
|
|
151
|
+
42,
|
|
152
|
+
str(tmp_path / "capture;still-one-argument"),
|
|
153
|
+
trigger_after_secs=0,
|
|
154
|
+
timeout_secs=1,
|
|
155
|
+
command=str(command),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
assert result["focused_target_window"] is True
|
|
159
|
+
assert started["arguments"][1:3] == ["inject", "--PID=42"]
|
|
160
|
+
assert started["arguments"][4] == str((tmp_path / "capture;still-one-argument").resolve())
|
|
161
|
+
assert started["kwargs"]["shell"] is False
|
|
162
|
+
assert started["process"].terminated is True
|
|
163
|
+
assert started["process"].killed is False
|
|
164
|
+
assert target["running"] is True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_capture_process_readiness_timeout_force_stops_only_injector(tmp_path, monkeypatch):
|
|
168
|
+
command = tmp_path / "renderdoccmd.exe"
|
|
169
|
+
command.touch()
|
|
170
|
+
target = {"running": True}
|
|
171
|
+
started = {}
|
|
172
|
+
|
|
173
|
+
class StuckInjector:
|
|
174
|
+
def __init__(self, arguments, **kwargs):
|
|
175
|
+
started["process"] = self
|
|
176
|
+
self.stdout = io.StringIO("")
|
|
177
|
+
self.stderr = io.StringIO("")
|
|
178
|
+
self.killed = False
|
|
179
|
+
self.terminated = False
|
|
180
|
+
|
|
181
|
+
def poll(self):
|
|
182
|
+
return -9 if self.killed else None
|
|
183
|
+
|
|
184
|
+
def terminate(self):
|
|
185
|
+
self.terminated = True
|
|
186
|
+
|
|
187
|
+
def kill(self):
|
|
188
|
+
self.killed = True
|
|
189
|
+
|
|
190
|
+
def wait(self, timeout=None):
|
|
191
|
+
if not self.killed:
|
|
192
|
+
raise TimeoutExpired("renderdoccmd", timeout)
|
|
193
|
+
return -9
|
|
194
|
+
|
|
195
|
+
monkeypatch.setattr(runtime.subprocess, "Popen", StuckInjector)
|
|
196
|
+
|
|
197
|
+
with pytest.raises(runtime.RenderDocError, match="waiting for inject readiness"):
|
|
198
|
+
runtime.capture_process(
|
|
199
|
+
42,
|
|
200
|
+
str(tmp_path / "capture"),
|
|
201
|
+
trigger_after_secs=0,
|
|
202
|
+
timeout_secs=0,
|
|
203
|
+
command=str(command),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
assert started["process"].terminated is True
|
|
207
|
+
assert started["process"].killed is True
|
|
208
|
+
assert target["running"] is True
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_triggered_program_capture_keeps_controller_alive_until_capture(tmp_path, monkeypatch):
|
|
212
|
+
target = tmp_path / "game.exe"
|
|
213
|
+
target.touch()
|
|
214
|
+
command = tmp_path / "renderdoccmd.exe"
|
|
215
|
+
command.touch()
|
|
216
|
+
events = []
|
|
217
|
+
|
|
218
|
+
class Controller:
|
|
219
|
+
def __init__(self, arguments, **kwargs):
|
|
220
|
+
events.append(("started", arguments, kwargs))
|
|
221
|
+
self.stdout = io.StringIO("Launched as ID 77\n")
|
|
222
|
+
self.stderr = io.StringIO("")
|
|
223
|
+
self.returncode = None
|
|
224
|
+
|
|
225
|
+
def poll(self):
|
|
226
|
+
return self.returncode
|
|
227
|
+
|
|
228
|
+
def terminate(self):
|
|
229
|
+
events.append(("stopped",))
|
|
230
|
+
self.returncode = 0
|
|
231
|
+
|
|
232
|
+
def kill(self):
|
|
233
|
+
raise AssertionError("responsive controller should not be killed")
|
|
234
|
+
|
|
235
|
+
def wait(self, timeout=None):
|
|
236
|
+
return self.returncode
|
|
237
|
+
|
|
238
|
+
monkeypatch.setattr(runtime.subprocess, "Popen", Controller)
|
|
239
|
+
monkeypatch.setattr(runtime, "_visible_process_ids", lambda _name: [])
|
|
240
|
+
|
|
241
|
+
def captured_while_running(directory, before, name, ignored, timeout):
|
|
242
|
+
assert [event[0] for event in events] == ["started"]
|
|
243
|
+
return 77, True, [tmp_path / "capture_frame1.rdc"]
|
|
244
|
+
|
|
245
|
+
monkeypatch.setattr(runtime, "_wait_for_triggered_capture", captured_while_running)
|
|
246
|
+
|
|
247
|
+
result = runtime.capture_program(
|
|
248
|
+
str(target),
|
|
249
|
+
str(tmp_path / "capture"),
|
|
250
|
+
trigger_after_secs=0,
|
|
251
|
+
command=str(command),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
assert result["captures"] == [str((tmp_path / "capture_frame1.rdc").resolve())]
|
|
255
|
+
assert [event[0] for event in events] == ["started", "stopped"]
|
|
256
|
+
assert events[0][1][-1] == str(target.resolve())
|
|
257
|
+
assert events[0][2]["shell"] is False
|
|
258
|
+
|
|
259
|
+
|
|
98
260
|
def test_capture_program_focuses_requested_child_before_trigger(tmp_path, monkeypatch):
|
|
99
261
|
target = tmp_path / "launcher.exe"
|
|
100
262
|
target.touch()
|
|
101
263
|
observed = {}
|
|
102
264
|
monkeypatch.setattr(
|
|
103
265
|
runtime,
|
|
104
|
-
"
|
|
105
|
-
lambda arguments, **kwargs:
|
|
266
|
+
"_start_capture_controller",
|
|
267
|
+
lambda arguments, **kwargs: StubController(),
|
|
106
268
|
)
|
|
107
269
|
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
108
270
|
monkeypatch.setattr(
|
|
@@ -131,8 +293,8 @@ def test_capture_program_failure_reports_missing_child_diagnostics(tmp_path, mon
|
|
|
131
293
|
target.touch()
|
|
132
294
|
monkeypatch.setattr(
|
|
133
295
|
runtime,
|
|
134
|
-
"
|
|
135
|
-
lambda arguments, **kwargs:
|
|
296
|
+
"_start_capture_controller",
|
|
297
|
+
lambda arguments, **kwargs: StubController(),
|
|
136
298
|
)
|
|
137
299
|
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
138
300
|
monkeypatch.setattr(
|
|
@@ -203,8 +365,8 @@ def test_capture_program_injects_visible_unhooked_child(tmp_path, monkeypatch):
|
|
|
203
365
|
observed = {}
|
|
204
366
|
monkeypatch.setattr(
|
|
205
367
|
runtime,
|
|
206
|
-
"
|
|
207
|
-
lambda arguments, **kwargs:
|
|
368
|
+
"_start_capture_controller",
|
|
369
|
+
lambda arguments, **kwargs: StubController(),
|
|
208
370
|
)
|
|
209
371
|
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
210
372
|
monkeypatch.setattr(
|
|
@@ -239,10 +401,8 @@ def test_capture_program_injects_visible_unhooked_child(tmp_path, monkeypatch):
|
|
|
239
401
|
def test_capture_process_failure_reports_injection_diagnostics(tmp_path, monkeypatch):
|
|
240
402
|
monkeypatch.setattr(
|
|
241
403
|
runtime,
|
|
242
|
-
"
|
|
243
|
-
lambda arguments, **kwargs:
|
|
244
|
-
arguments, 42, "Launched as ID 42", "inject warning"
|
|
245
|
-
),
|
|
404
|
+
"_start_capture_controller",
|
|
405
|
+
lambda arguments, **kwargs: StubController("Launched as ID 42", "inject warning"),
|
|
246
406
|
)
|
|
247
407
|
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
248
408
|
monkeypatch.setattr(runtime, "_trigger_capture_hotkey", lambda pid: False)
|
|
@@ -257,6 +417,8 @@ def test_capture_process_failure_reports_injection_diagnostics(tmp_path, monkeyp
|
|
|
257
417
|
runtime.capture_process(42, str(tmp_path / "capture"))
|
|
258
418
|
|
|
259
419
|
message = str(error.value)
|
|
420
|
+
assert "before the target creates its graphics device" in message
|
|
421
|
+
assert "capture_program" in message
|
|
260
422
|
assert "target_process=game.exe(pid=42)" in message
|
|
261
423
|
assert "focused_target_window=False" in message
|
|
262
424
|
assert "RenderDoc output: Launched as ID 42\ninject warning" in message
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|